Compare commits
80 commits
Author | SHA1 | Date | |
---|---|---|---|
XenGi | 5bef1a36a9 | ||
XenGi | 88b6f30a59 | ||
XenGi | e03b06cf20 | ||
XenGi | b337c7a6a7 | ||
XenGi | e0baa020c3 | ||
XenGi | b343500d92 | ||
XenGi | 5b53465e2c | ||
XenGi | 912a42c450 | ||
XenGi | 599a7284b9 | ||
XenGi | adb0190b3f | ||
XenGi | 1978208b61 | ||
XenGi | 73f65e00ba | ||
XenGi | 534077bad3 | ||
XenGi | 67335c97b4 | ||
XenGi | ab01f8f329 | ||
XenGi | 5474934cea | ||
XenGi | 1705de7391 | ||
XenGi | 62598c805e | ||
XenGi | cdf12411a2 | ||
XenGi | 26f230e039 | ||
XenGi | 9248852713 | ||
XenGi | 1fc5dc2bf8 | ||
XenGi | aeaef11341 | ||
XenGi | 44dd0ddd9c | ||
XenGi | cfbb8a47f7 | ||
XenGi | 58f59017dd | ||
XenGi | 2c2efaaca2 | ||
XenGi | bb19b095b6 | ||
XenGi | bddc82399b | ||
XenGi | bdff7622c5 | ||
XenGi | 899dde0754 | ||
XenGi | c8bc85f19e | ||
XenGi | 584f044503 | ||
XenGi | 03677e24be | ||
XenGi | 79a6049a91 | ||
XenGi | 7d56123998 | ||
XenGi | 55dd18d93c | ||
XenGi | 96b1d5ccf5 | ||
XenGi | fbd482d876 | ||
XenGi | 671bcd300f | ||
XenGi | 83297238c2 | ||
coon | 20c4dda160 | ||
coon | 61647a1f57 | ||
XenGi | df17949dfd | ||
XenGi | 755ff32c64 | ||
XenGi | 0cb3d62721 | ||
XenGi | 5efe18e222 | ||
XenGi | e60c43b376 | ||
XenGi | 53a61181b3 | ||
XenGi | 26e14afe33 | ||
XenGi | af1746287c | ||
XenGi | 5bb4b1688a | ||
XenGi | d52ba880cd | ||
XenGi | bc2f64eefd | ||
XenGi | 93f896fe90 | ||
XenGi | f6683ed78f | ||
coon | 5e9ff24647 | ||
XenGi | e38a94264b | ||
XenGi | 09e21904ae | ||
XenGi | 4b58b5189f | ||
XenGi | ce6e8ecf21 | ||
XenGi | 8881505d57 | ||
XenGi | cfaa05f7b5 | ||
coon | 8161b5d92b | ||
XenGi | 170cff5bdf | ||
XenGi | 343cf980ef | ||
XenGi | fa7e9bd497 | ||
coon | fc366aac3d | ||
XenGi | d97a33b800 | ||
XenGi | 3bdf933493 | ||
XenGi | 1c0fc195b0 | ||
XenGi | 3e635d9e44 | ||
XenGi | f65918db8b | ||
XenGi | 114d6ba4da | ||
XenGi | dd41374467 | ||
XenGi | 99d50084f2 | ||
XenGi | f34ffa4f37 | ||
XenGi | c29f255163 | ||
XenGi | af8b50d464 | ||
XenGi | 99dc857d41 |
|
@ -1,11 +0,0 @@
|
||||||
node_modules
|
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
README.md
|
|
||||||
LICENSE
|
|
||||||
.editorconfig
|
|
||||||
.idea
|
|
||||||
coverage*
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
name: Run tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- next
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
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 tests
|
|
||||||
run: make test
|
|
||||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,5 +1,10 @@
|
||||||
sanic
|
sanic
|
||||||
result
|
result
|
||||||
|
# self-signed certs for testing
|
||||||
|
cert.pem
|
||||||
|
key.pem
|
||||||
|
# test files
|
||||||
|
*.mp3
|
||||||
|
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/linux,windows,macos,vim,goland+all,go,direnv
|
# Created by https://www.toptal.com/developers/gitignore/api/linux,windows,macos,vim,goland+all,go,direnv
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=linux,windows,macos,vim,goland+all,go,direnv
|
# Edit at https://www.toptal.com/developers/gitignore?templates=linux,windows,macos,vim,goland+all,go,direnv
|
||||||
|
|
73
.gitlab-ci.yml
Normal file
73
.gitlab-ci.yml
Normal file
|
@ -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
|
22
Dockerfile
Normal file
22
Dockerfile
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
FROM docker.io/golang:1.22 as builder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download && go mod verify
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -v -o sanic ./...
|
||||||
|
|
||||||
|
# -----
|
||||||
|
|
||||||
|
FROM scratch as runner
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=builder /usr/src/app/sanic /sanic
|
||||||
|
COPY --from=builder /usr/src/app/static /static
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sanic"]
|
50
Makefile
50
Makefile
|
@ -1,3 +1,49 @@
|
||||||
mpd:
|
PROJECT := sanic
|
||||||
mkdir -p /tmp/sanic/{music,playlists}
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
.PHONY: mpd run build tidy verify test cert container help
|
||||||
|
|
||||||
|
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
|
mpd --no-daemon ./mpd.conf
|
||||||
|
|
||||||
|
run: build ## Run project
|
||||||
|
./${PROJECT}
|
||||||
|
|
||||||
|
build: ## Compile project
|
||||||
|
go build -v -o ${PROJECT}
|
||||||
|
|
||||||
|
update: ## Update go dependencies
|
||||||
|
go get -u
|
||||||
|
which gomod2nix && gomod2nix # sync go deps with nix
|
||||||
|
|
||||||
|
tidy: ## Add missing and remove unused modules
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
verify: ## Verify dependencies have expected content
|
||||||
|
go mod verify
|
||||||
|
|
||||||
|
format: ## Format go code
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
lint: ## Run linter (staticcheck)
|
||||||
|
staticcheck -f stylish ./...
|
||||||
|
|
||||||
|
test: ## Run tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
cert: ## Create https certificate for local testing
|
||||||
|
go run $$GOROOT/src/crypto/tls/generate_cert.go --host localhost
|
||||||
|
|
||||||
|
build-container: ## Build container image
|
||||||
|
podman build --tag ${PROJECT}:latest .
|
||||||
|
|
||||||
|
run-container: build-container ## Run container image
|
||||||
|
podman run --rm --volume ./config.ini:/config.ini --publish-all ${PROJECT}:latest
|
||||||
|
|
||||||
|
help: ## Display this help
|
||||||
|
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
|
102
NOTES.md
102
NOTES.md
|
@ -1,20 +1,86 @@
|
||||||
# mpd commands
|
# features
|
||||||
|
|
||||||
- [x] previous track
|
## frontend
|
||||||
- [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
|
|
||||||
|
|
||||||
|
- Ribbon menu
|
||||||
|
- [x] Show mpd connection state
|
||||||
|
- [ ] Display config dialog (is this even needed?)
|
||||||
|
- [x] `Update DB` button
|
||||||
|
- [x] Disable if running update is detected
|
||||||
|
- [x] `Previous Track` button
|
||||||
|
- [x] `Next Track` button
|
||||||
|
- [x] `Stop` button
|
||||||
|
- [x] `Play` button
|
||||||
|
- [x] `Pause` button
|
||||||
|
- [x] Track seeker
|
||||||
|
- [x] `Repeat` toggle
|
||||||
|
- [x] `Shuffle` toggle
|
||||||
|
- [ ] xfade
|
||||||
|
- [ ] decrease
|
||||||
|
- [ ] increase
|
||||||
|
- [x] Volume
|
||||||
|
- [x] increase
|
||||||
|
- [x] decrease
|
||||||
|
- [x] set with bar
|
||||||
|
- [x] `Now playing`
|
||||||
|
- [x] shows current track
|
||||||
|
- [x] marquee effect
|
||||||
|
- [x] `Time`
|
||||||
|
- Queue
|
||||||
|
- [x] Show queue
|
||||||
|
- [x] Highlight current track
|
||||||
|
- [ ] Move track up
|
||||||
|
- [ ] Move track down
|
||||||
|
- [ ] Remove track
|
||||||
|
- [ ] `Clear queue` button?
|
||||||
|
- File browser
|
||||||
|
- [ ] List all directories
|
||||||
|
- [ ] Selected folder has different icon (📂 vs 📁)
|
||||||
|
- [ ] Folders with subfolders have a ➕ or ➖ sign
|
||||||
|
- [ ] Clicked folders contents are displayed in the results
|
||||||
|
- [ ] Select tracks in results
|
||||||
|
- [ ] `Add` selected tracks to queue button
|
||||||
|
- Search
|
||||||
|
- [ ] Search files results
|
||||||
|
- [ ] Select tracks in results
|
||||||
|
- [ ] `Add` selected tracks to queue button
|
||||||
|
- Playlist browser
|
||||||
|
- [x] Show current playlists
|
||||||
|
- [x] `Replace` current queue with playlist button
|
||||||
|
- [x] `Attach` playlist to current queue button
|
||||||
|
- [x] `Save` current queue as playlist button
|
||||||
|
- [x] Show dialog
|
||||||
|
- [x] `Delete` selected playlist button
|
||||||
|
|
||||||
|
## backend
|
||||||
|
|
||||||
|
- Websocket
|
||||||
|
- [x] `#status` requests mpd infos:
|
||||||
|
- `status`
|
||||||
|
- `currentsong`
|
||||||
|
- `playlistinfo` (queue)
|
||||||
|
- [ ] `#download` requests download of URL (`yt-dlp`)
|
||||||
|
- *TBA*
|
||||||
|
- API endpoints
|
||||||
|
- [x] GET `/api/update_db`
|
||||||
|
- [x] GET `/api/previous_track`
|
||||||
|
- [x] GET `/api/next_track`
|
||||||
|
- [x] GET `/api/stop`
|
||||||
|
- [x] GET `/api/play`
|
||||||
|
- [x] GET `/api/pause`
|
||||||
|
- [x] GET `/api/seek/:seconds`
|
||||||
|
- [x] GET `/api/repeat`
|
||||||
|
- [x] GET `/api/random`
|
||||||
|
- [x] GET `/api/volume/:level`
|
||||||
|
- [ ] `/api/xfade/:seconds`
|
||||||
|
- [ ] `/api/queue_clear`
|
||||||
|
- [ ] POST `/api/queue` `{"song_id": 123}`
|
||||||
|
- [x] GET `/api/queue/:song_id/delete`
|
||||||
|
- [x] GET `/api/queue/:song_id/move/:position`
|
||||||
|
- [x] GET `/api/queue/replace/:playlist_name`
|
||||||
|
- [x] GET `/api/queue/attach/:playlist_name`
|
||||||
|
- [ ] GET `/api/database/:path`
|
||||||
|
- [x] GET `/api/playlists`
|
||||||
|
- [x] POST `/api/playlists/:name`
|
||||||
|
- [x] GET `/api/playlists/:name`
|
||||||
|
- [x] DELETE `/api/playlists/:name`
|
||||||
|
|
113
README.md
113
README.md
|
@ -1,34 +1,127 @@
|
||||||
[![maintained](https://img.shields.io/maintenance/yes/2023)]()
|
[![maintained](https://img.shields.io/maintenance/yes/2024?style=flat-square)]()
|
||||||
|
![Gitea Release](https://img.shields.io/gitea/v/release/cccb/sanic?gitea_url=https%3A%2F%2Fgit.berlin.ccc.de&sort=semver&display_name=release&style=flat-square)
|
||||||
|
|
||||||
# sanic
|
|
||||||
|
# 🦔 sanic
|
||||||
|
|
||||||
chaos music control inspired by [relaxx player][relaxx]
|
chaos music control inspired by [relaxx player][relaxx]
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
## features
|
|
||||||
|
|
||||||
- mpd web gui
|
- mpd web gui
|
||||||
- search music
|
- search music
|
||||||
- organize playlists
|
- organize playlists
|
||||||
- control current playback queue
|
- control current playback queue
|
||||||
- no authentication required to control music playback
|
- no authentication required to control music playback
|
||||||
- add music from other sources like youtube (`youtube-dl`)
|
|
||||||
- add playlists from internet radios (`*.m3u`, `*.pls`)
|
- add playlists from internet radios (`*.m3u`, `*.pls`)
|
||||||
|
- add music from other sources like youtube (`youtube-dl`)
|
||||||
|
|
||||||
## development
|
## 👩💻 Installation
|
||||||
|
|
||||||
Build nix flake:
|
### ❄️ NixOS (flakes)
|
||||||
|
|
||||||
|
Example flake setup (untested):
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
description = "Example Flake to install sanic on your host";
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = github:NixOS/nixpkgs/nixos-24.05;
|
||||||
|
sanic = {
|
||||||
|
url = gitlab:XenGi/sanic/main;
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
outputs = { self, nixpkgs, sanic }:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nixosConfigurations."myhostname".nixpkgs.lib.nixosSystem = {
|
||||||
|
inherit system;
|
||||||
|
modules = [
|
||||||
|
{ environment.systemPackages = [ sanic.packages.${system}.default ]; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🇦 Arch Linux
|
||||||
|
|
||||||
|
Install from the AUR:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
yay -S sanic
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🐳 Container
|
||||||
|
|
||||||
|
Run as daemon:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
podman run -d -v ./config.ini:/config.ini -p 8080:8080 registry.gitlab.com/XenGi/sanic:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
sanic is developed using [Nix][nix], but you can also just use the usual Golang tooling.
|
||||||
|
|
||||||
|
Run local [MPD][mpd] instance for testing with `make mpd`.
|
||||||
|
|
||||||
|
Update go dependencies like this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go get -u # or `make update`
|
||||||
|
go mod tidy # or `make tidy`
|
||||||
|
gomod2nix # sync go deps with nix
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❄️ w/ Nix
|
||||||
|
|
||||||
|
Enter development shell (also has [mpc][mpc] client installed for testing):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nix develop
|
||||||
|
```
|
||||||
|
|
||||||
|
Build sanic:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
nix build
|
nix build
|
||||||
```
|
```
|
||||||
|
|
||||||
## architecture
|
### 🐧 w/o Nix
|
||||||
|
|
||||||
[![Architecture](https://git.berlin.ccc.de/cccb/sanic/raw/branch/main/architecture.drawio.svg)](https://app.diagrams.net/?mode=git.berlin.ccc.de#Hcccb%2Fsanic%2Fmain%2Farchitecture.drawio.svg)
|
Use these Make targets for your convenience:
|
||||||
|
|
||||||
|
- `run`: Run project
|
||||||
|
- `build`: Compile project
|
||||||
|
- `tidy`: Add missing and remove unused modules
|
||||||
|
- `verify`: Verify dependencies have expected content
|
||||||
|
- `format`: Format go code
|
||||||
|
- `lint`: Run linter (staticcheck)
|
||||||
|
- `test`: Run tests
|
||||||
|
- `cert`: Create https certificate for local testing
|
||||||
|
|
||||||
|
### 🐳 Container
|
||||||
|
|
||||||
|
You can run sanic in a container. Use these Make targets for convenience:
|
||||||
|
|
||||||
|
- `build-container`: Build container image
|
||||||
|
- `run-container`: Run container image
|
||||||
|
|
||||||
|
## 🗺️ Architecture
|
||||||
|
|
||||||
|
[![Architecture](https://gitlab.com/XenGi/sanic/-/raw/main/architecture.drawio.svg)](https://app.diagrams.net/?mode=gitlab.com#AXenGi%2Fsanic%2Fmain%2Farchitecture.drawio.svg)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Made with ❤️ and 🐍.
|
Made with ❤️ and ![golang logo][golang].
|
||||||
|
|
||||||
[relaxx]: http://relaxx.dirk-hoeschen.de/
|
[relaxx]: http://relaxx.dirk-hoeschen.de/
|
||||||
|
[nix]: https://nixos.org/manual/nix/stable/
|
||||||
|
[golang]: https://go.dev/images/favicon-gopher.svg
|
||||||
|
[mpd]: https://musicpd.org/
|
||||||
|
[mpc]: https://www.musicpd.org/clients/mpc/
|
||||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
53
aur/PKGBUILD
Normal file
53
aur/PKGBUILD
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# Maintainer: Ricardo Band <email@ricardo.band>
|
||||||
|
|
||||||
|
pkgname=sanic
|
||||||
|
pkgver=0.0.1
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="chaos music control inspired by relaxx player"
|
||||||
|
arch=('any')
|
||||||
|
url=https://git.berlin.ccc.de/cccb/sanic
|
||||||
|
license=('custom:MIT')
|
||||||
|
makedepends=('go')
|
||||||
|
source=("$pkgname.service"
|
||||||
|
"$pkgname.sysusers"
|
||||||
|
"$pkgname.tmpfiles"
|
||||||
|
"$url/archive/v$pkgver.tar.gz")
|
||||||
|
sha256sums=("1337deadbeef"
|
||||||
|
"1337deadbeef"
|
||||||
|
"1337deadbeef"
|
||||||
|
"1337deadbeef")
|
||||||
|
|
||||||
|
prepare() {
|
||||||
|
cd "$pkgname-$pkgver"
|
||||||
|
|
||||||
|
mkdir -p build/
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$pkgname-$pkgver"
|
||||||
|
|
||||||
|
export CGO_CPPFLAGS="$CPPFLAGS"
|
||||||
|
export CGO_CFLAGS="$CFLAGS"
|
||||||
|
export CGO_CXXFLAGS="$CXXFLAGS"
|
||||||
|
export CGO_LDFLAGS="$LDFLAGS"
|
||||||
|
export GOFLAGS="-buildmode=pie -trimpath -ldflags=-linkmode=external -mod=readonly -modcacherw"
|
||||||
|
|
||||||
|
go build -o build/ .
|
||||||
|
}
|
||||||
|
|
||||||
|
check() {
|
||||||
|
cd "$pkgname-$pkgver"
|
||||||
|
|
||||||
|
go test ./...
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$pkgname-$pkgver"
|
||||||
|
|
||||||
|
install -Dm644 "LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||||
|
install -Dm755 build/$pkgname "$pkgdir"/usr/bin/$pkgname
|
||||||
|
install -Dm644 "../$pkgname.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service"
|
||||||
|
install -Dm644 "../$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
|
||||||
|
install -Dm644 "../$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
|
||||||
|
}
|
||||||
|
|
28
aur/sanic.service
Normal file
28
aur/sanic.service
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
[Unit]
|
||||||
|
Description=chaos music control
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=sanic
|
||||||
|
Group=sanic
|
||||||
|
ExecStart=/usr/bin/sanic
|
||||||
|
Restart=always
|
||||||
|
# security
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
StateDirectory=sanic
|
||||||
|
StateDirectoryMode=0750
|
||||||
|
ConfigurationDirectory=sanic
|
||||||
|
ConfigurationDirectoryMode=0750
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
3
aur/sanic.sysusers
Normal file
3
aur/sanic.sysusers
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
u sanic - "chaos music control" /run/sanic /usr/bin/nologin
|
||||||
|
g sanic - -
|
||||||
|
|
3
aur/sanic.tmpfiles
Normal file
3
aur/sanic.tmpfiles
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
d /etc/sanic 0750 sanic sanic
|
||||||
|
d /run/sanic 0750 sanic sanic
|
||||||
|
|
18
cert.pem
18
cert.pem
|
@ -1,18 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIC+jCCAeKgAwIBAgIRAKMiYz6LLo+9sgp/p9YbsgIwDQYJKoZIhvcNAQELBQAw
|
|
||||||
EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0yMzExMDcyMDAyNTRaFw0yNDExMDYyMDAy
|
|
||||||
NTRaMBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
|
||||||
ggEKAoIBAQCkb+0FSWggYNPP0+X1erZc+9Qe1SxYv868C5FYgh6OZG8+YU7/7aYC
|
|
||||||
trk30LysZpO3gi9XfVWv1+R5c1DBXisxtrgz5Z966qBk60yBnPeigu7rtnHnxX3p
|
|
||||||
JDs0Yd9CLCGk6g4zHbCuDVqNHxWjc/M8oQMCb9ay5Qh0fFEOR6G7b0x9z7eXUCnT
|
|
||||||
+rZ+SvXpuZ75kxTYQNK7taYPEK2abP79u+rv3DXKR8nMWUwtligCCTj8dTAUPiVW
|
|
||||||
nQJ4MTv4Xj0gkak4n1p+r2BPrWvTjQOj8ndag5fe8sufkVu3Slt2yY0Ipdk15jfC
|
|
||||||
p6/C8FpZVTstwjP/XCAOV2se+FE5qwgVAgMBAAGjSzBJMA4GA1UdDwEB/wQEAwIF
|
|
||||||
oDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuC
|
|
||||||
CWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAFGeg/L0jJn/IIeD37jn/GQ28
|
|
||||||
lCHe0RJC3x1FH/v5uNgnZXqz+xDV8SUv84wj9kbUnfu1gPaVhpwDMuirHrYhgViN
|
|
||||||
Fq742zLnUxZW7TOnIR/nPGEvfQRhe8wLWvkkT1pRVH1yJbdge431vIiuBEjOBEBO
|
|
||||||
cr2DZWL6xiJACSG0FBI6Iizvz4vbXrEHhFmleI4Gvj2WR8qx/A29rTnh+oE7ymMo
|
|
||||||
+yhREeDEZ7kTXTevGZoIwfHeHMg+IE1bNFz1OlA3qb38PS5ko+uD4INlT9f2dDdP
|
|
||||||
7uMJFAXjk6jqoZ5CRBuLCRSeTj5ady6x9X8lBv8be9np6JFue6L3S+d1/OoucQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
12
config.ini
Normal file
12
config.ini
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[mpd]
|
||||||
|
host = localhost
|
||||||
|
port = 6600
|
||||||
|
#username =
|
||||||
|
#pasword =
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
hostname = [::1]
|
||||||
|
port = 443
|
||||||
|
tls = yes
|
||||||
|
cert = cert.pem
|
||||||
|
key = key.pem
|
BIN
favicon.xcf
Normal file
BIN
favicon.xcf
Normal file
Binary file not shown.
18
flake.lock
18
flake.lock
|
@ -5,11 +5,11 @@
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1694529238,
|
"lastModified": 1710146030,
|
||||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -28,11 +28,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1699950847,
|
"lastModified": 1722589758,
|
||||||
"narHash": "sha256-xN/yVtqHb7kimHA/WvQFrEG5WS38t0K+A/W+j/WhQWM=",
|
"narHash": "sha256-sbbA8b6Q2vB/t/r1znHawoXLysCyD4L/6n6/RykiSnA=",
|
||||||
"owner": "tweag",
|
"owner": "tweag",
|
||||||
"repo": "gomod2nix",
|
"repo": "gomod2nix",
|
||||||
"rev": "05c993c9a5bd55a629cd45ed49951557b7e9c61a",
|
"rev": "4e08ca09253ef996bd4c03afa383b23e35fe28a1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -43,11 +43,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1701237617,
|
"lastModified": 1723221148,
|
||||||
"narHash": "sha256-Ryd8xpNDY9MJnBFDYhB37XSFIxCPVVVXAbInNPa95vs=",
|
"narHash": "sha256-7pjpeQlZUNQ4eeVntytU3jkw9dFK3k1Htgk2iuXjaD8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "85306ef2470ba705c97ce72741d56e42d0264015",
|
"rev": "154bcb95ad51bc257c2ce4043a725de6ca700ef6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
143
flake.nix
143
flake.nix
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
description = "chaos music control";
|
description = "sanic - chaos music control";
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = github:NixOS/nixpkgs/nixpkgs-unstable;
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = github:numtide/flake-utils;
|
||||||
gomod2nix = {
|
gomod2nix = {
|
||||||
url = "github:tweag/gomod2nix";
|
url = github:tweag/gomod2nix;
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
inputs.flake-utils.follows = "flake-utils";
|
inputs.flake-utils.follows = "flake-utils";
|
||||||
};
|
};
|
||||||
|
@ -21,22 +21,145 @@
|
||||||
src = ./.;
|
src = ./.;
|
||||||
modules = ./gomod2nix.toml;
|
modules = ./gomod2nix.toml;
|
||||||
};
|
};
|
||||||
in {
|
in
|
||||||
defaultPackage = sanic;
|
{
|
||||||
|
formatter = pkgs.nixpkgs-fmt;
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
go
|
go
|
||||||
gopls
|
go-tools # staticcheck
|
||||||
gotools
|
|
||||||
go-tools
|
|
||||||
gomod2nix.packages.${system}.default
|
gomod2nix.packages.${system}.default
|
||||||
sanic
|
|
||||||
];
|
];
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
mpd
|
mpd
|
||||||
mpc-cli
|
mpc-cli
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
packages.default = sanic;
|
||||||
|
nixosModules.default = { config, lib, pkgs, options, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.services.sanic;
|
||||||
|
configFile = pkgs.writeText "config.ini" ''
|
||||||
|
[ui]
|
||||||
|
host=${cfg.ui.host}
|
||||||
|
port=${toString cfg.ui.port}
|
||||||
|
tls=${if cfg.ui.tls then "true" else "false"}
|
||||||
|
certificate=${toString cfg.ui.certificate}
|
||||||
|
key=${toString cfg.ui.key}
|
||||||
|
|
||||||
|
[mpd]
|
||||||
|
host=${cfg.backend.host}
|
||||||
|
port=${toString cfg.backend.port}
|
||||||
|
'';
|
||||||
|
execCommand = "${cfg.package}/bin/sanic -c '${configFile}'";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.sanic = {
|
||||||
|
enable = lib.mkEnableOption "Enables the sanic systemd service.";
|
||||||
|
package = lib.mkOption {
|
||||||
|
description = "Package to use.";
|
||||||
|
type = lib.types.package;
|
||||||
|
default = sanic;
|
||||||
|
};
|
||||||
|
ui = lib.mkOption {
|
||||||
|
description = "Setting for HTTP(S) UI.";
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
host = "[::1]";
|
||||||
|
port = 443;
|
||||||
|
tls = true;
|
||||||
|
certificate = "${config.security.acme.certs."sanic.example.com".directory}/fullchain.pem";
|
||||||
|
key = "${config.security.acme.certs."sanic.example.com".directory}/key.pem";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
default = {
|
||||||
|
host = "[::1]";
|
||||||
|
port = 80;
|
||||||
|
tls = false;
|
||||||
|
};
|
||||||
|
type = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
host = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "[::1]";
|
||||||
|
description = "Host to bind to.";
|
||||||
|
};
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 80;
|
||||||
|
description = "Port to listen on.";
|
||||||
|
};
|
||||||
|
tls = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enables HTTPS.";
|
||||||
|
};
|
||||||
|
certificate = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Path to TLS certificate for HTTPS.";
|
||||||
|
};
|
||||||
|
key = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Path to TLS key for HTTPS.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
backend = lib.mkOption {
|
||||||
|
description = "Configure MPD backend.";
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
host = "localhost";
|
||||||
|
port = 6600;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
default = {
|
||||||
|
host = "localhost";
|
||||||
|
port = 6600;
|
||||||
|
};
|
||||||
|
type = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
host = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "localhost";
|
||||||
|
description = "Hostname or IP of MPD instance.";
|
||||||
|
};
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 6600;
|
||||||
|
description = "Port of MPD instance.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
systemd.services."sanic" = {
|
||||||
|
description = "sanic - chaos music control";
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = 30;
|
||||||
|
ExecStart = execCommand;
|
||||||
|
User = "sanic";
|
||||||
|
Group = "sanic";
|
||||||
|
AmbientCapabilities = lib.mkIf (cfg.ui.port < 1000) [ "CAP_NET_BIND_SERVICE" ];
|
||||||
|
CapabilityBoundingSet = lib.mkIf (cfg.ui.port < 1000) [ "CAP_NET_BIND_SERVICE" ];
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
};
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#meta = {
|
||||||
|
# maintainers = with lib.maintainers; [ xengi ];
|
||||||
|
# doc = ./default.xml;
|
||||||
|
#};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
41
go.mod
41
go.mod
|
@ -1,35 +1,32 @@
|
||||||
module github.com/cccb/sanic
|
module gitlab.com/XenGi/sanic
|
||||||
|
|
||||||
go 1.20
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fhs/gompd/v2 v2.3.0
|
github.com/fhs/gompd/v2 v2.3.0
|
||||||
github.com/labstack/echo-contrib v0.15.0
|
github.com/labstack/echo-contrib v0.17.1
|
||||||
github.com/labstack/echo/v4 v4.11.2
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
golang.org/x/net v0.17.0
|
golang.org/x/net v0.28.0
|
||||||
|
gopkg.in/ini.v1 v1.67.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/labstack/gommon v0.4.0 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
github.com/prometheus/common v0.55.0 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/prometheus/common v0.40.0 // indirect
|
|
||||||
github.com/prometheus/procfs v0.9.0 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/sys v0.23.0 // indirect
|
||||||
golang.org/x/text v0.13.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.6.0 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
)
|
)
|
||||||
|
|
124
go.sum
124
go.sum
|
@ -1,85 +1,79 @@
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fhs/gompd/v2 v2.3.0 h1:wuruUjmOODRlJhrYx73rJnzS7vTSXSU7pWmZtM3VPE0=
|
github.com/fhs/gompd/v2 v2.3.0 h1:wuruUjmOODRlJhrYx73rJnzS7vTSXSU7pWmZtM3VPE0=
|
||||||
github.com/fhs/gompd/v2 v2.3.0/go.mod h1:nNdZtcpD5VpmzZbRl5rV6RhxeMmAWTxEsSIMBkmMIy4=
|
github.com/fhs/gompd/v2 v2.3.0/go.mod h1:nNdZtcpD5VpmzZbRl5rV6RhxeMmAWTxEsSIMBkmMIy4=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/labstack/echo-contrib v0.17.0 h1:xam8wakZOsiQYM14Z0og1xF3w/heWNeDF5AtC5PlX8E=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/labstack/echo-contrib v0.17.0/go.mod h1:mjX5VB3OqJcroIEycptBOY9Hr7rK+unq79W8QFKGNV0=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/labstack/echo-contrib v0.17.1 h1:7I/he7ylVKsDUieaGRZ9XxxTYOjfQwVzHzUYrNykfCU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/labstack/echo-contrib v0.17.1/go.mod h1:SnsCZtwHBAZm5uBSAtQtXQHI3wqEA73hvTn0bYMKnZA=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
github.com/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU=
|
|
||||||
github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4=
|
|
||||||
github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE=
|
|
||||||
github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
|
|
||||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
|
||||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
|
||||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||||
github.com/prometheus/common v0.40.0 h1:Afz7EVRqGg2Mqqf4JuF9vdvp1pi220m55Pi9T2JnO4Q=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/common v0.40.0/go.mod h1:L65ZJPSmfn/UBWLQIHV7dBrKFidB/wPlF1y5TlSt9OE=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA=
|
||||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
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=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
|
||||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -5,56 +5,44 @@ schema = 3
|
||||||
version = "v1.0.1"
|
version = "v1.0.1"
|
||||||
hash = "sha256-h75GUqfwJKngCJQVE5Ao5wnO3cfKD9lSIteoLp/3xJ4="
|
hash = "sha256-h75GUqfwJKngCJQVE5Ao5wnO3cfKD9lSIteoLp/3xJ4="
|
||||||
[mod."github.com/cespare/xxhash/v2"]
|
[mod."github.com/cespare/xxhash/v2"]
|
||||||
version = "v2.2.0"
|
version = "v2.3.0"
|
||||||
hash = "sha256-nPufwYQfTkyrEkbBrpqM3C2vnMxfIz6tAaBmiUP7vd4="
|
hash = "sha256-7hRlwSR+fos1kx4VZmJ/7snR7zHh8ZFKX+qqqqGcQpY="
|
||||||
[mod."github.com/fhs/gompd/v2"]
|
[mod."github.com/fhs/gompd/v2"]
|
||||||
version = "v2.3.0"
|
version = "v2.3.0"
|
||||||
hash = "sha256-JBb7BvLu1wlUAbMt/g5JmJtA3fxqr6dKWeeLwfGsB08="
|
hash = "sha256-JBb7BvLu1wlUAbMt/g5JmJtA3fxqr6dKWeeLwfGsB08="
|
||||||
[mod."github.com/golang-jwt/jwt"]
|
[mod."github.com/golang-jwt/jwt"]
|
||||||
version = "v3.2.2+incompatible"
|
version = "v3.2.2+incompatible"
|
||||||
hash = "sha256-LOkpuXhWrFayvVf1GOaOmZI5YKEsgqVSb22aF8LnCEM="
|
hash = "sha256-LOkpuXhWrFayvVf1GOaOmZI5YKEsgqVSb22aF8LnCEM="
|
||||||
[mod."github.com/golang/protobuf"]
|
|
||||||
version = "v1.5.2"
|
|
||||||
hash = "sha256-IVwooaIo46iq7euSSVWTBAdKd+2DUaJ67MtBao1DpBI="
|
|
||||||
[mod."github.com/json-iterator/go"]
|
|
||||||
version = "v1.1.12"
|
|
||||||
hash = "sha256-To8A0h+lbfZ/6zM+2PpRpY3+L6725OPC66lffq6fUoM="
|
|
||||||
[mod."github.com/labstack/echo-contrib"]
|
[mod."github.com/labstack/echo-contrib"]
|
||||||
version = "v0.15.0"
|
version = "v0.17.1"
|
||||||
hash = "sha256-bDjEAJc5gPs+G5M8fbTSBFgb0t4dTYqdECyvHvuf3gY="
|
hash = "sha256-OkO1gWz1xL8Rw2wy4RMJjsLGhABAqAsjpVkQ3b6G+L4="
|
||||||
[mod."github.com/labstack/echo/v4"]
|
[mod."github.com/labstack/echo/v4"]
|
||||||
version = "v4.11.2"
|
version = "v4.12.0"
|
||||||
hash = "sha256-OECk2lBNKKBpzJ58XMhpp8KI/tqE0TnyddWyhI+nHPs="
|
hash = "sha256-TPXJv/6C53bnmcEYxa9g5Mft8u/rLT96q64tQ9+RtKU="
|
||||||
[mod."github.com/labstack/gommon"]
|
[mod."github.com/labstack/gommon"]
|
||||||
version = "v0.4.0"
|
version = "v0.4.2"
|
||||||
hash = "sha256-xISAIJEu2xh0hoWsORbgjnz3rDK3ft3hrvmxt0wfHVw="
|
hash = "sha256-395+BETDpv15L2lsCiEccwakXgEQxKHlYBAU0Ot3qhY="
|
||||||
[mod."github.com/mattn/go-colorable"]
|
[mod."github.com/mattn/go-colorable"]
|
||||||
version = "v0.1.13"
|
version = "v0.1.13"
|
||||||
hash = "sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8="
|
hash = "sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8="
|
||||||
[mod."github.com/mattn/go-isatty"]
|
[mod."github.com/mattn/go-isatty"]
|
||||||
version = "v0.0.19"
|
version = "v0.0.20"
|
||||||
hash = "sha256-wYQqGxeqV3Elkmn26Md8mKZ/viw598R4Ych3vtt72YE="
|
hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ="
|
||||||
[mod."github.com/matttproud/golang_protobuf_extensions"]
|
[mod."github.com/munnerz/goautoneg"]
|
||||||
version = "v1.0.4"
|
version = "v0.0.0-20191010083416-a7dc8b61c822"
|
||||||
hash = "sha256-uovu7OycdeZ2oYQ7FhVxLey5ZX3T0FzShaRldndyGvc="
|
hash = "sha256-79URDDFenmGc9JZu+5AXHToMrtTREHb3BC84b/gym9Q="
|
||||||
[mod."github.com/modern-go/concurrent"]
|
|
||||||
version = "v0.0.0-20180306012644-bacd9c7ef1dd"
|
|
||||||
hash = "sha256-OTySieAgPWR4oJnlohaFTeK1tRaVp/b0d1rYY8xKMzo="
|
|
||||||
[mod."github.com/modern-go/reflect2"]
|
|
||||||
version = "v1.0.2"
|
|
||||||
hash = "sha256-+W9EIW7okXIXjWEgOaMh58eLvBZ7OshW2EhaIpNLSBU="
|
|
||||||
[mod."github.com/prometheus/client_golang"]
|
[mod."github.com/prometheus/client_golang"]
|
||||||
version = "v1.14.0"
|
version = "v1.19.1"
|
||||||
hash = "sha256-dpgGV8C30ZCn7b9mQ+Ye2AfPXTIuHLQbl2olMKzJKxA="
|
hash = "sha256-MSLsMDi89uQc7Pa2fhqeamyfPJpenGj3r+eB/UotK7w="
|
||||||
[mod."github.com/prometheus/client_model"]
|
[mod."github.com/prometheus/client_model"]
|
||||||
version = "v0.3.0"
|
version = "v0.6.1"
|
||||||
hash = "sha256-vP+miJfsoK5UG9eug8z/bhAMj3bwg66T2vIh8WHoOKU="
|
hash = "sha256-rIDyUzNfxRA934PIoySR0EhuBbZVRK/25Jlc/r8WODw="
|
||||||
[mod."github.com/prometheus/common"]
|
[mod."github.com/prometheus/common"]
|
||||||
version = "v0.40.0"
|
version = "v0.55.0"
|
||||||
hash = "sha256-ykOktYSNsCYKR4Ru7UWoebIgzMqJjLG2jrbmO7NBv08="
|
hash = "sha256-qzvCnc+hnAB5dq2MYy8GlPxgyNnyn9kFVlN2CXZe9T0="
|
||||||
[mod."github.com/prometheus/procfs"]
|
[mod."github.com/prometheus/procfs"]
|
||||||
version = "v0.9.0"
|
version = "v0.15.1"
|
||||||
hash = "sha256-imZN+1HRpMvgmrot2V+AK5ueYLmsp49vZfHtx2N6Wek="
|
hash = "sha256-H+WXJemFFwdoglmD6p7JRjrJJZmIVAmJwYmLbZ8Q9sw="
|
||||||
[mod."github.com/valyala/bytebufferpool"]
|
[mod."github.com/valyala/bytebufferpool"]
|
||||||
version = "v1.0.0"
|
version = "v1.0.0"
|
||||||
hash = "sha256-I9FPZ3kCNRB+o0dpMwBnwZ35Fj9+ThvITn8a3Jr8mAY="
|
hash = "sha256-I9FPZ3kCNRB+o0dpMwBnwZ35Fj9+ThvITn8a3Jr8mAY="
|
||||||
|
@ -62,20 +50,23 @@ schema = 3
|
||||||
version = "v1.2.2"
|
version = "v1.2.2"
|
||||||
hash = "sha256-gp+lNXE8zjO+qJDM/YbS6V43HFsYP6PKn4ux1qa5lZ0="
|
hash = "sha256-gp+lNXE8zjO+qJDM/YbS6V43HFsYP6PKn4ux1qa5lZ0="
|
||||||
[mod."golang.org/x/crypto"]
|
[mod."golang.org/x/crypto"]
|
||||||
version = "v0.14.0"
|
version = "v0.26.0"
|
||||||
hash = "sha256-UUSt3X/i34r1K0mU+Y5IzljX5HYy07JcHh39Pm1MU+o="
|
hash = "sha256-Iicrsb65fCmjfPILKoSLyBZMwe2VUcoTF5SpYTCQEuk="
|
||||||
[mod."golang.org/x/net"]
|
[mod."golang.org/x/net"]
|
||||||
version = "v0.17.0"
|
version = "v0.28.0"
|
||||||
hash = "sha256-qRawHWLSsJ06QNbLhUWPXGVSO1eaioeC9xZlUEWN8J8="
|
hash = "sha256-WdH/mgsX/CB+CiYtXEwJAXHN8FgtW2YhFcWwrrHNBLo="
|
||||||
[mod."golang.org/x/sys"]
|
[mod."golang.org/x/sys"]
|
||||||
version = "v0.13.0"
|
version = "v0.23.0"
|
||||||
hash = "sha256-/+RDZ0a0oEfJ0k304VqpJpdrl2ZXa3yFlOxy4mjW7w0="
|
hash = "sha256-tC6QVLu72bADgINz26FUGdmYqKgsU45bHPg7sa0ZV7w="
|
||||||
[mod."golang.org/x/text"]
|
[mod."golang.org/x/text"]
|
||||||
version = "v0.13.0"
|
version = "v0.17.0"
|
||||||
hash = "sha256-J34dbc8UNVIdRJUZP7jPt11oxuwG8VvrOOylxE7V3oA="
|
hash = "sha256-R8JbsP7KX+KFTHH7SjRnUGCdvtagylVOfngWEnVSqBc="
|
||||||
[mod."golang.org/x/time"]
|
[mod."golang.org/x/time"]
|
||||||
version = "v0.3.0"
|
version = "v0.6.0"
|
||||||
hash = "sha256-/hmc9skIswMYbivxNS7R8A6vCTUF9k2/7tr/ACkcEaM="
|
hash = "sha256-gW9TVK9HjLk52lzfo5rBzSunc01gS0+SG2nk0X1w55M="
|
||||||
[mod."google.golang.org/protobuf"]
|
[mod."google.golang.org/protobuf"]
|
||||||
version = "v1.28.1"
|
version = "v1.34.2"
|
||||||
hash = "sha256-sTJYgvlv5is7vHNxcuigF2lNASp0QonhUgnrguhfHSU="
|
hash = "sha256-nMTlrDEE2dbpWz50eQMPBQXCyQh4IdjrTIccaU0F3m0="
|
||||||
|
[mod."gopkg.in/ini.v1"]
|
||||||
|
version = "v1.67.0"
|
||||||
|
hash = "sha256-V10ahGNGT+NLRdKUyRg1dos5RxLBXBk1xutcnquc/+4="
|
||||||
|
|
28
key.pem
28
key.pem
|
@ -1,28 +0,0 @@
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkb+0FSWggYNPP
|
|
||||||
0+X1erZc+9Qe1SxYv868C5FYgh6OZG8+YU7/7aYCtrk30LysZpO3gi9XfVWv1+R5
|
|
||||||
c1DBXisxtrgz5Z966qBk60yBnPeigu7rtnHnxX3pJDs0Yd9CLCGk6g4zHbCuDVqN
|
|
||||||
HxWjc/M8oQMCb9ay5Qh0fFEOR6G7b0x9z7eXUCnT+rZ+SvXpuZ75kxTYQNK7taYP
|
|
||||||
EK2abP79u+rv3DXKR8nMWUwtligCCTj8dTAUPiVWnQJ4MTv4Xj0gkak4n1p+r2BP
|
|
||||||
rWvTjQOj8ndag5fe8sufkVu3Slt2yY0Ipdk15jfCp6/C8FpZVTstwjP/XCAOV2se
|
|
||||||
+FE5qwgVAgMBAAECggEAZN9QTBzy5mHfra80czla44GMZ3mn4a2QWaTS/bOcoQcV
|
|
||||||
gvPYrYEQhLuotYe0JiIq3bjb59S4Gs3al3JZCLYG01FegF+NTK7fw+jgHua2tpeR
|
|
||||||
j0F1cJOjIMEcHI8pkQNXhD4chdjhyHPip415DbMUdI3lNbp7v3RmkmasppDkswCg
|
|
||||||
yz+vLflXwGTliuNzM0HxAPl+gH70KxqXgLMqGH568f8D0b63cViarfRCkL2UnFK6
|
|
||||||
rbysWIsRDqG8CIHZZya5KcTruDDXY0DXnZaXahINgb62Lm4C0tqlF387VWhREMyV
|
|
||||||
KM4DNFXsiDcgQ6El3BzhxMI0boI02X3jx4lPT11hAQKBgQDU3SlEzrgIX8gM7uG7
|
|
||||||
oIXaivHnSR/vYD4tCXtgHDrcNlToNEbFi/73ZQKuiNZHOUQWwDw59Y5LtQwOnpch
|
|
||||||
1cqI3E1Eq8CMNP+yAnnbbeDQTwqhou4nU87zpnXY+jVDnktHAF5zIwZo/8f6DaEu
|
|
||||||
TDZx3AAGKHv3ScZC6eprsWBNwQKBgQDFwoKvSp55ggY95ZJ9XXAUkeqH2LeGMPun
|
|
||||||
126vGviOdBM7c4Rqm4UQ/pPc/UDG3/CcvXpWf/Ab5SkXZJPRWJfd23RdIZcj5PX0
|
|
||||||
mRpXdaCJoLfFo0hH4V5C2O3zdIzip9pOU7P5WyWKf68qd7fpNuGUux/vhUnuOaue
|
|
||||||
ksraLKz3VQKBgQCiVozLsg++KzYJTwGOs2yB8GduaWFWkQK6HDogYUcufK0ibgPv
|
|
||||||
UsY/bKSv8SHiLbVU2ITV+wTrjgbE+4PtRPvyhnjTP11YUG8VFjhS9ah3lWBZR0Xz
|
|
||||||
bkItpazIroGCsS1d19UwX+zalP+xH0XmZi87hHnsOGHahUQT8gta2GrGwQKBgEi8
|
|
||||||
z2Z8EqfsjDEuBGB6AqR+Ov42VuJTl+xXD832JJ/4z64ZQgYYJ6xlhqtMtwuvCIgO
|
|
||||||
JTY1nnIUKrYA92GTrWAbvMQYe8fnChQqUAcFK8QTSiS2dvqBSTNbKMJYBw3C4UfX
|
|
||||||
/6Viwf8cEaiUGh/8YKJc5VUq+FNYAOi1Y0k2D3R1AoGAD+Ao5z3UplNCX+F6u/qa
|
|
||||||
XoGBFxUt6lFK+ROjXmUl7JmCHHgGuBZ/c6ZaNCFQWKOc21Z92en2whZS2afmjhYG
|
|
||||||
6Ps5CU0IKR5E616SOe38rfNpLeu/A+164oneTgb8m3UWAiJnTqTNFg8B1xzsRCBK
|
|
||||||
l5Fg6NTJ/tALKrhGfDEHHAU=
|
|
||||||
-----END PRIVATE KEY-----
|
|
454
mpd.go
Normal file
454
mpd.go
Normal file
|
@ -0,0 +1,454 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/fhs/gompd/v2/mpd"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MPD API calls
|
||||||
|
|
||||||
|
// updateDb Updates the music database: find new files, remove deleted files, update modified files.
|
||||||
|
func updateDb(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
jobId, err := conn.Update("")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, fmt.Sprintf("Database update started with job id %d", jobId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// previousTrack Plays previous song in the queue.
|
||||||
|
func previousTrack(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
err = conn.Previous()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, "Playing previous track in queue")
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextTrack Plays next song in the queue.
|
||||||
|
func nextTrack(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
err = conn.Next()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, "PLaying next track in queue")
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopPlayback Stops playing.
|
||||||
|
func stopPlayback(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
err = conn.Stop()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, "Playback stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
// resumePlayback Begins playing the playlist or if paused resumes playback.
|
||||||
|
func resumePlayback(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
status, err := conn.Status()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
if status["state"] == "stop" {
|
||||||
|
err := conn.Play(-1)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = conn.Pause(false)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, "Playback resumed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// pausePlayback Pauses playback.
|
||||||
|
func pausePlayback(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
err = conn.Pause(true)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, "Playback paused")
|
||||||
|
}
|
||||||
|
|
||||||
|
// seek Seeks to the position defined by seconds within the current song.
|
||||||
|
func seek(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
seconds, err := strconv.Atoi(c.Param("seconds"))
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if seconds < 0 {
|
||||||
|
return c.String(http.StatusBadRequest, "seconds must be positive integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Duration type seems to be used incorrectly
|
||||||
|
err = conn.SeekCur(time.Duration(seconds)*time.Second, false)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, fmt.Sprintf("Seeked current track to %d seconds", seconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggleRepeat Toggles repeat state between 1 or 0.
|
||||||
|
func toggleRepeat(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
status, err := conn.Status()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
var msg string
|
||||||
|
if status["repeat"] == "1" {
|
||||||
|
err = conn.Repeat(false)
|
||||||
|
msg = "Toggled Repeat mode to off"
|
||||||
|
} else {
|
||||||
|
err = conn.Repeat(true)
|
||||||
|
msg = "Toggled Repeat mode to on"
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggleRandom Toggles random state between 1 or 0.
|
||||||
|
func toggleRandom(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
status, err := conn.Status()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
var msg string
|
||||||
|
if status["random"] == "1" {
|
||||||
|
err = conn.Random(false)
|
||||||
|
msg = "Toggled Random mode to off"
|
||||||
|
} else {
|
||||||
|
err = conn.Random(true)
|
||||||
|
msg = "Toggled Random mode to on"
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setVolume Sets volume to level, the range of volume is 0-100.
|
||||||
|
func setVolume(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
level, err := strconv.Atoi(c.Param("level"))
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(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 {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, fmt.Sprintf("Set volume to %d", level))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue
|
||||||
|
|
||||||
|
// deleteTrackFromQueue removed track with song_id from queue
|
||||||
|
func deleteTrackFromQueue(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
songId, err := strconv.Atoi(c.Param("song_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.DeleteID(songId)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, fmt.Sprintf("Removed song %d from queue", songId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveTrackInQueue moves song with song_id to the new place position in the queue.
|
||||||
|
func moveTrackInQueue(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
songId, err := strconv.Atoi(c.Param("song_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
position, err := strconv.Atoi(c.Param("position"))
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.MoveID(songId, position)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, fmt.Sprintf("Moved song %d to position %d", songId, position))
|
||||||
|
}
|
||||||
|
|
||||||
|
// attachPlaylist adds the playlist with the name playlist_name to the queue.
|
||||||
|
func attachPlaylist(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
name := c.Param("playlist_name")
|
||||||
|
|
||||||
|
err = conn.PlaylistLoad(name, -1, -1)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// replaceQueue replaces the current queue with the playlist with the name playlist_name.
|
||||||
|
func replaceQueue(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
name := c.Param("playlist_name")
|
||||||
|
|
||||||
|
err = conn.Clear()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
err = conn.PlaylistLoad(name, -1, -1)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playlists
|
||||||
|
|
||||||
|
// listPlaylists return a list of all stored playlists.
|
||||||
|
func listPlaylists(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
playlists, err := conn.ListPlaylists()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, playlists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listPlaylist returns the contents of the playlist defined by name.
|
||||||
|
func listPlaylist(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
name := c.Param("name")
|
||||||
|
|
||||||
|
playlist, err := conn.PlaylistContents(name)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deletePlaylist deletes the playlist defined by name.
|
||||||
|
func deletePlaylist(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
name := c.Param("name")
|
||||||
|
|
||||||
|
err = conn.PlaylistRemove(name)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusNoContent, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// savePlaylist saves the current queue to a playlist with the given name.
|
||||||
|
func savePlaylist(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
name := c.Param("name")
|
||||||
|
|
||||||
|
err = conn.PlaylistSave(name)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusCreated, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchDatabase search the database path given by pattern and returns all entries that contain the pattern either in their artist, album or title.
|
||||||
|
func searchDatabase(c echo.Context) error {
|
||||||
|
// Connect to MPD server
|
||||||
|
conn, err := mpd.Dial("tcp", "localhost:6600")
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
pattern := c.Param("pattern")
|
||||||
|
|
||||||
|
artistResult, err := conn.Search("artist", pattern)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
albumResult, err := conn.Search("album", pattern)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
titleResult, err := conn.Search("title", pattern)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
songs := append(append(artistResult, albumResult...), titleResult...)
|
||||||
|
|
||||||
|
// make list unique
|
||||||
|
uniqueList := make([]mpd.Attrs, 0, len(songs))
|
||||||
|
keep := make(map[string]bool)
|
||||||
|
for _, song := range songs {
|
||||||
|
if _, ok := keep[song["file"]]; !ok {
|
||||||
|
keep[song["file"]] = true
|
||||||
|
uniqueList = append(uniqueList, song)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, uniqueList)
|
||||||
|
}
|
29
sanic.container
Normal file
29
sanic.container
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
[Unit]
|
||||||
|
Description=sanic - chaos music control
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
AddCapability=CAP_NET_BIND_SERVICE
|
||||||
|
AutoUpdate=registry
|
||||||
|
ContainerName=sanic
|
||||||
|
Group=sanic
|
||||||
|
HealthCmd=/usr/bin/curl localhost:443/echo
|
||||||
|
HealthInterval=2m
|
||||||
|
HealthOnFailure=restart
|
||||||
|
HealthRetries=5
|
||||||
|
HealthStartPeriod=1m
|
||||||
|
Image=registry.gitlab.com/xengi/sanic/sanic:latest
|
||||||
|
LogDriver=journald
|
||||||
|
Network=host
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PublishPort=443
|
||||||
|
Pull=always
|
||||||
|
User=sanic
|
||||||
|
Volume=/etc/sanic/config.ini:/config.ini
|
||||||
|
|
||||||
|
[Service}
|
||||||
|
Restart=always
|
||||||
|
TimeoutStartSec=900
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target default.target
|
||||||
|
|
304
server.go
304
server.go
|
@ -2,19 +2,47 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fhs/gompd/v2/mpd"
|
|
||||||
"github.com/labstack/echo-contrib/echoprometheus"
|
"github.com/labstack/echo-contrib/echoprometheus"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
"golang.org/x/net/websocket"
|
"gopkg.in/ini.v1"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"os"
|
||||||
"strings"
|
"os/exec"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config holds the configuration for the mpd connection and for the web server.
|
||||||
|
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() {
|
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 := echo.New()
|
||||||
e.Use(middleware.Logger())
|
e.Use(middleware.Logger())
|
||||||
e.Use(middleware.Recover())
|
e.Use(middleware.Recover())
|
||||||
|
@ -45,7 +73,23 @@ func main() {
|
||||||
return c.File("index.html")
|
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 := `
|
||||||
|
<code>
|
||||||
|
Protocol: %s<br>
|
||||||
|
Host: %s<br>
|
||||||
|
Remote Address: %s<br>
|
||||||
|
Method: %s<br>
|
||||||
|
Path: %s<br>
|
||||||
|
</code>
|
||||||
|
`
|
||||||
|
return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path))
|
||||||
|
})
|
||||||
|
|
||||||
g := e.Group("/api")
|
g := e.Group("/api")
|
||||||
|
g.GET("/update_db", updateDb)
|
||||||
g.GET("/previous_track", previousTrack)
|
g.GET("/previous_track", previousTrack)
|
||||||
g.GET("/next_track", nextTrack)
|
g.GET("/next_track", nextTrack)
|
||||||
g.GET("/stop", stopPlayback)
|
g.GET("/stop", stopPlayback)
|
||||||
|
@ -56,224 +100,48 @@ func main() {
|
||||||
g.GET("/random", toggleRandom)
|
g.GET("/random", toggleRandom)
|
||||||
g.GET("/volume/:level", setVolume)
|
g.GET("/volume/:level", setVolume)
|
||||||
|
|
||||||
e.GET("/ws", wsServe)
|
g.GET("/queue/:song_id/delete", deleteTrackFromQueue)
|
||||||
|
g.GET("/queue/:song_id/move/:position", moveTrackInQueue)
|
||||||
|
g.GET("/queue/replace/:playlist_name", replaceQueue)
|
||||||
|
g.GET("/queue/attach/:playlist_name", attachPlaylist)
|
||||||
|
|
||||||
e.Logger.Fatal(e.StartTLS(":1323", "cert.pem", "key.pem"))
|
g.GET("/playlists", listPlaylists)
|
||||||
//e.Logger.Fatal(e.Start(":1323"))
|
g.POST("/playlists/:name", savePlaylist)
|
||||||
}
|
g.GET("/playlists/:name", listPlaylist)
|
||||||
|
g.DELETE("/playlists/:name", deletePlaylist)
|
||||||
|
|
||||||
func wsServe(c echo.Context) error {
|
g.GET("/database/:pattern", searchDatabase)
|
||||||
fmt.Println("wsServe")
|
|
||||||
websocket.Handler(func(ws *websocket.Conn) {
|
g.GET("/download", downloadTrack)
|
||||||
defer ws.Close()
|
|
||||||
fmt.Println("handler")
|
e.GET("/sse", serveSSE)
|
||||||
for {
|
|
||||||
// Read
|
if config.UI.Tls {
|
||||||
msg := ""
|
e.Logger.Fatal(e.StartTLS(fmt.Sprintf("%s:%d", config.UI.Hostname, config.UI.Port), config.UI.Certificate, config.UI.Key))
|
||||||
err := websocket.Message.Receive(ws, &msg)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Error(err)
|
|
||||||
break
|
|
||||||
} else {
|
} else {
|
||||||
if strings.HasPrefix(strings.ToUpper(msg), "MPD#") {
|
e.Logger.Fatal(e.Start(fmt.Sprintf("%s:%d", config.UI.Hostname, config.UI.Port)))
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//fmt.Println(msg)
|
|
||||||
}
|
|
||||||
}).ServeHTTP(c.Response(), c.Request())
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API calls
|
// downloadTrack tries to download a given URL and saves the song to the database.
|
||||||
|
func downloadTrack(c echo.Context) error {
|
||||||
func previousTrack(c echo.Context) error {
|
cmd := exec.Command(
|
||||||
// Connect to MPD server
|
"yt-dlp",
|
||||||
conn, err := mpd.Dial("tcp", "localhost:6600")
|
"--no-wait-for-video",
|
||||||
if err != nil {
|
"--no-playlist",
|
||||||
log.Fatalln(err)
|
"--windows-filenames",
|
||||||
}
|
"--newline",
|
||||||
defer conn.Close()
|
"--extract-audio",
|
||||||
|
"--audio-format", "mp3",
|
||||||
err = conn.Previous()
|
"--audio-quality", "0",
|
||||||
if err != nil {
|
"--format", "bestaudio/best",
|
||||||
log.Fatalln(err)
|
c.Param("url"),
|
||||||
|
)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
c.Logger().Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.String(http.StatusNoContent, "")
|
return c.String(http.StatusAccepted, "")
|
||||||
}
|
|
||||||
|
|
||||||
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, "")
|
|
||||||
}
|
}
|
||||||
|
|
30
services.nix
Normal file
30
services.nix
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{ self, ...}: {config, lib, pkgs, ...}:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.sanic;
|
||||||
|
format = pkgs.formats.ini { };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.sanic = {
|
||||||
|
enable = mkEnableOption (lib.mdDoc "sanic");
|
||||||
|
settings = mkOption {
|
||||||
|
type = format.type;
|
||||||
|
default = { };
|
||||||
|
description = lib.mkDoc ''
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
systemd.services.sanic = {
|
||||||
|
description = "chaos music control";
|
||||||
|
wantedBy = [ "multi-user.target" "default.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
DynamicUser = true;
|
||||||
|
ExecStart = "${self.packages.${pkgs.system}.default}/bin/sanic";
|
||||||
|
Restart = "on-failure";
|
||||||
|
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
195
sse.go
Normal file
195
sse.go
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
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()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
//c.Logger().Print("status " + string(jsonStatus))
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
//c.Logger().Print("current_song " + string(jsonCurrentSong))
|
||||||
|
// Only send new event if different from last time
|
||||||
|
if !bytes.Equal(jsonCurrentSong, lastJsonCurrentSong) {
|
||||||
|
currentSongEvent := Event{
|
||||||
|
Event: []byte("currentsong"),
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
//c.Logger().Print("queue " + string(jsonQueue))
|
||||||
|
// Only send new event if different from last time
|
||||||
|
if !bytes.Equal(jsonQueue, lastJsonQueue) {
|
||||||
|
queueEvent := Event{
|
||||||
|
Event: []byte("queue"),
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
|
@ -3,8 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Sanic - Flexbox layout</title>
|
<title>Sanic - Flexbox layout</title>
|
||||||
<link rel="stylesheet" href="../rangeinput.css">
|
<link rel="stylesheet" href="rangeinput.css">
|
||||||
<link rel="stylesheet" href="../treeview.css">
|
<link rel="stylesheet" href="treeview.css">
|
||||||
<link rel="stylesheet" href="sanic.css">
|
<link rel="stylesheet" href="sanic.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -158,6 +158,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="../sanic.js"></script>
|
<script src="sanic.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
100
static/flexbox/treeview.css
Normal file
100
static/flexbox/treeview.css
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
/* 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::before {
|
||||||
|
content : '';
|
||||||
|
display : block;
|
||||||
|
position : absolute;
|
||||||
|
top : calc(var(--spacing) - 8px);
|
||||||
|
left : 6px;
|
||||||
|
width : calc(var(--spacing) + 2px);
|
||||||
|
border : solid #ddd;
|
||||||
|
border-width : 0 0 2px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul li::before {
|
||||||
|
content : '';
|
||||||
|
display : block;
|
||||||
|
position : absolute;
|
||||||
|
top : calc(var(--spacing) - 22px);
|
||||||
|
left : -2px;
|
||||||
|
width : calc(var(--spacing) + 2px);
|
||||||
|
height : calc(var(--spacing) - 2px);
|
||||||
|
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 : '\1F4C1';
|
||||||
|
display : block;
|
||||||
|
position : absolute;
|
||||||
|
top : calc(var(--spacing) / 1.5 - var(--radius));
|
||||||
|
left : calc(var(--spacing) - var(--radius) - 1px);
|
||||||
|
width : calc(2 * var(--radius));
|
||||||
|
height : calc(2 * var(--radius));
|
||||||
|
}u
|
||||||
|
|
||||||
|
/* Expand and collapse buttons */
|
||||||
|
|
||||||
|
.tree summary::before {
|
||||||
|
z-index: 1;
|
||||||
|
content: '\1F4C1';
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree details[open] > summary::before {
|
||||||
|
content: '\1F4C2';
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="20">
|
|
||||||
<g fill="#fff">
|
|
||||||
<path d="m5 9h4v-4h2v4h4v2h-4v4h-2v-4h-4z"/>
|
|
||||||
<path d="m25 9h10v2h-10z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 199 B |
231
static/index.html
Normal file
231
static/index.html
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" xmlns="http://www.w3.org/1999/html">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Sanic</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="stylesheet" href="treeview.css">
|
||||||
|
<link rel="icon" href="favicon.ico" sizes="16x16 32x32 48x48 64x64" type="image/png">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<dialog id="save-playlist">
|
||||||
|
<h1>Save Playlist</h1>
|
||||||
|
<button class="close">×</button>
|
||||||
|
<form method="dialog">
|
||||||
|
<label for="control-playlist-name">Name</label>
|
||||||
|
<input type="text" id="control-playlist-name" name="playlist-name" autofocus>
|
||||||
|
<button>Save</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div id="nav">
|
||||||
|
<div id="control-admin">
|
||||||
|
<button id="connection-state">❌ Disconnected</button> <!-- ❌ Cross Mark -->
|
||||||
|
<button>Config</button>
|
||||||
|
<button id="control-update-db" data-jobid="" disabled="disabled"><span class="loader"></span> Update DB</button>
|
||||||
|
</div><!--/#control-admin-->
|
||||||
|
<div>
|
||||||
|
<div class="spaced">
|
||||||
|
<button id="control-previous">⏮︎</button> <!-- ⏮️ Last Track Button -->
|
||||||
|
<button id="control-stop">⏹︎</button> <!-- ⏹️ Stop Button -->
|
||||||
|
<button id="control-play-pause">⏵︎</button> <!-- ▶️ Play or ⏸️ Pause Button -->
|
||||||
|
<button id="control-next">⏭︎</button> <!-- ⏭️ Next Track Button -->
|
||||||
|
</div><!--/.spaced-->
|
||||||
|
<div class="spaced">
|
||||||
|
<label for="control-progress"></label>
|
||||||
|
<input type="range" id="control-progress" name="progress" min="0" step="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="spaced">
|
||||||
|
<button id="control-repeat" data-state="off">🔘 repeat</button> <!-- 🔘 Radio Button -->
|
||||||
|
<button id="control-shuffle" data-state="off">🔘 shuffle</button> <!-- 🔘 Radio Button -->
|
||||||
|
</div><!--/.spaced-->
|
||||||
|
<div class="spaced">
|
||||||
|
<label for="control-xfade">xfade</label>
|
||||||
|
<div>
|
||||||
|
<button id="control-xfade-minus">➖</button> <!-- ➖ Minus -->
|
||||||
|
<input type="number" id="control-xfade" name="xfade" value="00" />
|
||||||
|
<button id="control-xfade-plus">➕</button> <!-- ➕ Plus -->
|
||||||
|
</div>
|
||||||
|
</div><!--/.spaced-->
|
||||||
|
<div class="spaced">
|
||||||
|
<button id="control-volume-down">🔉</button> <!-- 🔉 Speaker with sound wave -->
|
||||||
|
<input id="control-volume" name="volume" type="range" min="0" max="100" value="50" />
|
||||||
|
<button id="control-volume-up">🔊</button> <!-- 🔊 Speaker with sound waves -->
|
||||||
|
</div><!--/.spaced-->
|
||||||
|
</div>
|
||||||
|
<div class="wide">
|
||||||
|
<div>
|
||||||
|
<label for="control-track">Now playing:</label>
|
||||||
|
<!--<input type="text" id="control-track" name="track" disabled="disabled" />-->
|
||||||
|
<div class="marquee" id="control-track" data-songid="">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="control-time">Time:</label>
|
||||||
|
<input type="text" id="control-time" name="time" value="00:00:00/00:00:00" disabled="disabled" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="sanic-logo">
|
||||||
|
<div><!-- TODO: try to remove this div -->
|
||||||
|
<img alt="sanic logo" src="img/sanic-logo.webp" />
|
||||||
|
Sanic © 2023
|
||||||
|
</div>
|
||||||
|
</div><!--/#sanic-logo-->
|
||||||
|
</div>
|
||||||
|
<div id="queue">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Pos</th>
|
||||||
|
<th>Artists</th>
|
||||||
|
<th>Track</th>
|
||||||
|
<th>Album</th>
|
||||||
|
<th>Length</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div><!--/#queue-->
|
||||||
|
<div id="browser">
|
||||||
|
<div id="tabs">
|
||||||
|
<a id="tab-browser" class="active" href="#">File Browser</a>
|
||||||
|
<a id="tab-search" href="#">Search</a>
|
||||||
|
<a id="tab-playlists" href="#">Playlists</a>
|
||||||
|
</div><!--/#tabs-->
|
||||||
|
<div id="file-browser">
|
||||||
|
<div>
|
||||||
|
<ul id="tree">
|
||||||
|
<li>
|
||||||
|
<input type="checkbox"/>
|
||||||
|
<span>00_music</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<input checked type="checkbox"/>
|
||||||
|
<span>autosort</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input checked type="checkbox"/>
|
||||||
|
<span>reimport</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input checked type="checkbox"/>
|
||||||
|
<span>unsortable</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input checked type="checkbox"/>
|
||||||
|
<span>youtube</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input checked type="checkbox"/>
|
||||||
|
<span>01_incoming</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<input checked type="checkbox"/>
|
||||||
|
<span>coon</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox"/>
|
||||||
|
<span>Rick Astley - Never Gonna Give You Up</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox"/>
|
||||||
|
<span>Nyan Cat</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox"/>
|
||||||
|
<span>cascha</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox"/>
|
||||||
|
<span>XenGi</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox"/>
|
||||||
|
<span>02_megablast</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<input checked type="checkbox"/>
|
||||||
|
<span>dnb</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input checked type="checkbox"/>
|
||||||
|
<span>mix</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox"/>
|
||||||
|
<span>03_mfs</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<input checked type="checkbox"/>
|
||||||
|
<span>ambient</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input checked type="checkbox"/>
|
||||||
|
<span>electronic</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
actions
|
||||||
|
</div>
|
||||||
|
</div><!--/#file-browser-->
|
||||||
|
<div id="search" style="display: none">
|
||||||
|
<div>
|
||||||
|
<input type="text" id="control-search-pattern" name="pattern">
|
||||||
|
<button id="control-search-submit">Search</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
actions
|
||||||
|
</div>
|
||||||
|
</div><!--/#search-->
|
||||||
|
<div id="playlist-browser" style="display: none">
|
||||||
|
<label for="control-playlist-list"></label>
|
||||||
|
<select id="control-playlist-list" size="15">
|
||||||
|
</select><!--/#control-playlist-list-->
|
||||||
|
<div>
|
||||||
|
<button id="control-refresh-playlists">🔄 Refresh</button><!-- 🔄 Counterclockwise Arrows Button -->
|
||||||
|
<button id="control-replace-playlist">⤴️ Replace</button><!-- ⤴️ Arrow Pointing Rightwards Then Curving Upwards -->
|
||||||
|
<button id="control-attach-playlist">⬆ Attach</button><!-- ⬆️ Up Arrow -->
|
||||||
|
<button id="control-save-playlist">💾 Save</button><!-- 💾 Floppy Disk -->
|
||||||
|
<button id="control-delete-playlist">🗑️ Delete</button><!-- 🗑️ Wastebasket -->
|
||||||
|
</div>
|
||||||
|
</div><!--/#playlist-browser-->
|
||||||
|
</div><!--/#browser-->
|
||||||
|
<div id="result">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Artist</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Album</th>
|
||||||
|
<th>Genre</th>
|
||||||
|
<th>Time</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div><!--/#result-->
|
||||||
|
<footer>
|
||||||
|
<a href="https://gitlab.com/XenGi/sanic"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 92 92"><defs><clipPath id="a"><path d="M0 .113h91.887V92H0Zm0 0"/></clipPath></defs><g clip-path="url(#a)"><path style="stroke:none;fill-rule:nonzero;fill:#ffffff;fill-opacity:1" d="M90.156 41.965 50.036 1.848a5.913 5.913 0 0 0-8.368 0l-8.332 8.332 10.566 10.566a7.03 7.03 0 0 1 7.23 1.684 7.043 7.043 0 0 1 1.673 7.277l10.183 10.184a7.026 7.026 0 0 1 7.278 1.672 7.04 7.04 0 0 1 0 9.957 7.045 7.045 0 0 1-9.961 0 7.038 7.038 0 0 1-1.532-7.66l-9.5-9.497V59.36a7.04 7.04 0 0 1 1.86 11.29 7.04 7.04 0 0 1-9.957 0 7.04 7.04 0 0 1 0-9.958 7.034 7.034 0 0 1 2.308-1.539V33.926a7.001 7.001 0 0 1-2.308-1.535 7.049 7.049 0 0 1-1.516-7.7L29.242 14.273 1.734 41.777a5.918 5.918 0 0 0 0 8.371L41.855 90.27a5.92 5.92 0 0 0 8.368 0l39.933-39.934a5.925 5.925 0 0 0 0-8.371"/></g></svg></a> Sanic MPD Web UI 0.1.0 - by XenGi and coon © 2023
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
<script src="index.js"></script>
|
||||||
|
<script src="sse.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
361
static/index.js
Normal file
361
static/index.js
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
// Configuration
|
||||||
|
|
||||||
|
const API_URL = `${document.location.protocol}//${document.location.host}/api`;
|
||||||
|
const VOLUME_STEP = 5;
|
||||||
|
|
||||||
|
// Get control elements
|
||||||
|
|
||||||
|
const dialog_save_playlist = document.getElementById("save-playlist");
|
||||||
|
const control_playlist_name = document.getElementById("control-playlist-name");
|
||||||
|
const dialog_save_playlist_submit = document.querySelector("#save-playlist form button");
|
||||||
|
const dialog_save_playlist_close = document.querySelector("#save-playlist .close");
|
||||||
|
const connection_state = document.getElementById("connection-state");
|
||||||
|
const control_update_db = document.getElementById("control-update-db");
|
||||||
|
const control_previous = document.getElementById("control-previous");
|
||||||
|
const control_play_pause = document.getElementById("control-play-pause");
|
||||||
|
const control_stop = document.getElementById("control-stop");
|
||||||
|
const control_next = document.getElementById("control-next");
|
||||||
|
const control_progress = document.getElementById("control-progress");
|
||||||
|
const control_repeat = document.getElementById("control-repeat");
|
||||||
|
const control_shuffle = document.getElementById("control-shuffle");
|
||||||
|
const control_xfade = document.getElementById("control-xfade");
|
||||||
|
const control_xfade_minus = document.getElementById("control-xfade-minus");
|
||||||
|
const control_xfade_plus = document.getElementById("control-xfade-plus");
|
||||||
|
const control_volume = document.getElementById("control-volume");
|
||||||
|
const control_volume_up = document.getElementById("control-volume-up");
|
||||||
|
const control_volume_down = document.getElementById("control-volume-down");
|
||||||
|
const queue_table = document.querySelector("#queue tbody");
|
||||||
|
const control_track = document.getElementById("control-track");
|
||||||
|
const control_time = document.getElementById("control-time");
|
||||||
|
const tabs = document.getElementById("tabs");
|
||||||
|
const tab_browser = document.getElementById("tab-browser");
|
||||||
|
const tab_search = document.getElementById("tab-search");
|
||||||
|
const tab_playlists = document.getElementById("tab-playlists");
|
||||||
|
const control_playlist_list = document.getElementById("control-playlist-list");
|
||||||
|
const control_refresh_playlists = document.getElementById("control-refresh-playlists");
|
||||||
|
const control_replace_playlist = document.getElementById("control-replace-playlist");
|
||||||
|
const control_attach_playlist = document.getElementById("control-attach-playlist");
|
||||||
|
const control_save_playlist = document.getElementById("control-save-playlist");
|
||||||
|
const control_delete_playlist = document.getElementById("control-delete-playlist");
|
||||||
|
const result_table = document.querySelector("#result tbody");
|
||||||
|
const control_search_pattern = document.getElementById("control-search-pattern");
|
||||||
|
const control_search_submit = document.getElementById("control-search-submit");
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
|
||||||
|
secondsToTrackTime = (t) => {
|
||||||
|
const hours = Math.floor(t / 3600);
|
||||||
|
const minutes = Math.floor((t - hours * 3600) / 60);
|
||||||
|
const seconds = Math.floor(t - hours * 3600 - minutes * 60);
|
||||||
|
|
||||||
|
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTrackFromQueue = (event) => {
|
||||||
|
const song_id = event.target.parentElement.parentElement.dataset.song_id;
|
||||||
|
|
||||||
|
console.log(`DEBUG: remove song id ${song_id} from queue`);
|
||||||
|
fetch(`${API_URL}/queue/${song_id}/delete`).then(r => {
|
||||||
|
console.log(r.text());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTrackInQueue = (event, direction) => {
|
||||||
|
const song_id = event.target.parentElement.parentElement.dataset.song_id;
|
||||||
|
// TODO: figure out position in queue by counting HTML elements?
|
||||||
|
const position = parseInt(event.target.parentElement.parentElement.firstChild.innerText);
|
||||||
|
|
||||||
|
console.log(`DEBUG: move song ${song_id} down in queue to position ${position + direction}`);
|
||||||
|
fetch(`${API_URL}/queue/${song_id}/move/${position + direction}`).then(r => {
|
||||||
|
console.log(r.text());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPlaylists = () => {
|
||||||
|
console.log("Refreshing playlists from MPD server")
|
||||||
|
fetch(`${API_URL}/playlists`).then(async r => {
|
||||||
|
if (r.status === 200) {
|
||||||
|
const playlists = await r.json();
|
||||||
|
control_playlist_list.options.length = 0; // clear playlists
|
||||||
|
playlists.forEach(p => {
|
||||||
|
const option = document.createElement("option")
|
||||||
|
option.innerText = p["playlist"];
|
||||||
|
option.value = p["playlist"];
|
||||||
|
option.addEventListener("click", () => {
|
||||||
|
fetch(`${API_URL}/playlists/${p["playlist"]}`).then(async r => {
|
||||||
|
if (r.status === 200) fillResultTable(await r.json());
|
||||||
|
})
|
||||||
|
});
|
||||||
|
control_playlist_list.appendChild(option)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fillResultTable = (songs) => {
|
||||||
|
result_table.innerHTML = "";
|
||||||
|
songs.forEach(song => {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
const artist = document.createElement("td");
|
||||||
|
artist.innerText = song["Artist"];
|
||||||
|
const title = document.createElement("td");
|
||||||
|
title.innerText = song["Title"];
|
||||||
|
const time = document.createElement("td");
|
||||||
|
time.innerText = secondsToTrackTime(parseInt(song["Time"]))
|
||||||
|
tr.appendChild(artist);
|
||||||
|
tr.appendChild(title);
|
||||||
|
tr.appendChild(document.createElement("td")); // album
|
||||||
|
tr.appendChild(document.createElement("td")); // genre
|
||||||
|
tr.appendChild(time);
|
||||||
|
result_table.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI controls
|
||||||
|
|
||||||
|
tab_browser.addEventListener("click", () => {
|
||||||
|
if (!tab_browser.classList.contains("active")) {
|
||||||
|
tab_browser.classList.add("active");
|
||||||
|
tab_search.classList.remove("active")
|
||||||
|
tab_playlists.classList.remove("active")
|
||||||
|
document.getElementById("file-browser").style.display = "block";
|
||||||
|
document.getElementById("search").style.display = "none";
|
||||||
|
document.getElementById("playlist-browser").style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tab_search.addEventListener("click", () => {
|
||||||
|
if (!tab_search.classList.contains("active")) {
|
||||||
|
tab_browser.classList.remove("active");
|
||||||
|
tab_search.classList.add("active")
|
||||||
|
tab_playlists.classList.remove("active")
|
||||||
|
document.getElementById("file-browser").style.display = "none";
|
||||||
|
document.getElementById("search").style.display = "block";
|
||||||
|
document.getElementById("playlist-browser").style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tab_playlists.addEventListener("click", () => {
|
||||||
|
refreshPlaylists();
|
||||||
|
if (!tab_playlists.classList.contains("active")) {
|
||||||
|
tab_browser.classList.remove("active");
|
||||||
|
tab_search.classList.remove("active")
|
||||||
|
tab_playlists.classList.add("active")
|
||||||
|
document.getElementById("file-browser").style.display = "none";
|
||||||
|
document.getElementById("search").style.display = "none";
|
||||||
|
document.getElementById("playlist-browser").style.display = "block";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show "Save playlist" modal
|
||||||
|
control_save_playlist.addEventListener("click", () => {
|
||||||
|
dialog_save_playlist.showModal()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close "Save playlist" modal
|
||||||
|
dialog_save_playlist_close.addEventListener("click", () => {
|
||||||
|
dialog_save_playlist.close()
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Add API calls to controls
|
||||||
|
|
||||||
|
control_search_submit.addEventListener("click", event => {
|
||||||
|
event.preventDefault()
|
||||||
|
fetch(`${API_URL}/database/${control_search_pattern.value}`).then(async r => {
|
||||||
|
if (r.status === 200) {
|
||||||
|
fillResultTable([...new Set(await r.json())]);
|
||||||
|
} else {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
control_refresh_playlists.addEventListener("click", () => {
|
||||||
|
refreshPlaylists();
|
||||||
|
});
|
||||||
|
|
||||||
|
control_replace_playlist.addEventListener("click", () => {
|
||||||
|
fetch(`${API_URL}/queue/replace/${control_playlist_list.value}`).then(async r => {
|
||||||
|
if (r.status !== 200) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_attach_playlist.addEventListener("click", () => {
|
||||||
|
fetch(`${API_URL}/queue/attach/${control_playlist_list.value}`).then(async r => {
|
||||||
|
if (r.status !== 200) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save current queue as new playlist and refresh playlist list
|
||||||
|
dialog_save_playlist_submit.addEventListener("click", () => {
|
||||||
|
fetch(`${API_URL}/playlists/${control_playlist_name.value}`, {method: "POST"}).then(async r => {
|
||||||
|
if (r.status === 201) {
|
||||||
|
console.log(`Playlist "${control_playlist_name.value}" saved`)
|
||||||
|
refreshPlaylists()
|
||||||
|
} else {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_delete_playlist.addEventListener("click", () => {
|
||||||
|
const playlist_name = control_playlist_list.value;
|
||||||
|
fetch(`${API_URL}/playlists/${control_playlist_list.value}`, {method: "DELETE"}).then(r => {
|
||||||
|
if (r.status === 204) {
|
||||||
|
console.log(`Playlist "${playlist_name}" successfully deleted.`);
|
||||||
|
refreshPlaylists();
|
||||||
|
} else {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add API calls to controls
|
||||||
|
|
||||||
|
control_update_db.addEventListener("click", (event) => {
|
||||||
|
console.log("Issuing database update");
|
||||||
|
fetch(`${API_URL}/update_db`).then(async r => {
|
||||||
|
if (r.status === 200) {
|
||||||
|
// const idText = await r.text();
|
||||||
|
console.log(await r.text());
|
||||||
|
// event.target.dataset.jobid = idText.split(" ").pop();
|
||||||
|
// event.target.disabled = true;
|
||||||
|
} else {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_previous.addEventListener("click", () => {
|
||||||
|
fetch(`${API_URL}/previous_track`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_play_pause.addEventListener("click", event => {
|
||||||
|
if (event.target.innerHTML === "⏸︎") { // Resume playback
|
||||||
|
fetch(`${API_URL}/pause`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else { // Pause playback
|
||||||
|
fetch(`${API_URL}/play`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
control_stop.addEventListener("click", () => {
|
||||||
|
fetch(`${API_URL}/stop`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_next.addEventListener("click", () => {
|
||||||
|
fetch(`${API_URL}/next_track`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_progress.addEventListener("change", event => {
|
||||||
|
fetch(`${API_URL}/seek/${event.target.value}`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_progress.addEventListener("input", event => {
|
||||||
|
control_time.value = `${secondsToTrackTime(event.target.value)}/${secondsToTrackTime(event.target.max)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
control_repeat.addEventListener("click", event => {
|
||||||
|
if (event.target.dataset.state === "on") { // TODO: check is never true
|
||||||
|
event.target.innerHTML = "🔘 repeat";
|
||||||
|
event.target.dataset.state = "off";
|
||||||
|
} else {
|
||||||
|
event.target.innerHTML = "🔴 repeat";
|
||||||
|
event.target.dataset.state = "on";
|
||||||
|
}
|
||||||
|
fetch(`${API_URL}/repeat`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_shuffle.addEventListener("click", event => {
|
||||||
|
if (event.target.dataset.state === "on") { // TODO: check is never true
|
||||||
|
event.target.innerHTML = "🔘 shuffle";
|
||||||
|
event.target.dataset.state = "off";
|
||||||
|
} else {
|
||||||
|
event.target.innerHTML = "🔴 shuffle";
|
||||||
|
event.target.dataset.state = "on";
|
||||||
|
}
|
||||||
|
fetch(`${API_URL}/random`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_xfade_minus.addEventListener("click", () => {
|
||||||
|
// TODO: not yet implemented
|
||||||
|
fetch(`${API_URL}/xfade`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_xfade_plus.addEventListener("click", () => {
|
||||||
|
// TODO: not yet implemented
|
||||||
|
fetch(`${API_URL}/xfade`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
control_volume_up.addEventListener("click", () => {
|
||||||
|
const volume = Math.min(parseInt(control_volume.value) + VOLUME_STEP, 100);
|
||||||
|
fetch(`${API_URL}/volume/${volume}`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
control_volume.value = volume;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
control_volume_down.addEventListener("click", () => {
|
||||||
|
const volume = Math.max(parseInt(control_volume.value) - VOLUME_STEP, 0);
|
||||||
|
fetch(`${API_URL}/volume/${volume}`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
control_volume.value = volume;
|
||||||
|
});
|
||||||
|
|
||||||
|
control_volume.addEventListener("change", event => {
|
||||||
|
fetch(`${API_URL}/volume/${event.target.value}`).then(async r => {
|
||||||
|
if (r.status >= 400) {
|
||||||
|
console.error(`API returned ${r.status}: ${r.statusText}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
178
static/sse.js
Normal file
178
static/sse.js
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
// Server-Sent-Events
|
||||||
|
|
||||||
|
if (typeof (EventSource) !== "undefined") {
|
||||||
|
const sse = new EventSource("/sse");
|
||||||
|
sse.addEventListener("status", handleStatus);
|
||||||
|
sse.addEventListener("currentsong", handleCurrentSong);
|
||||||
|
sse.addEventListener("queue", handleQueue);
|
||||||
|
sse.onmessage = (event) => {
|
||||||
|
console.log("sse message: " + event.data);
|
||||||
|
};
|
||||||
|
sse.onerror = (err) => {
|
||||||
|
console.error("EventSource failed:", err);
|
||||||
|
connection_state.innerHTML = "❌ Disconnected"; // ❌ Cross Mark
|
||||||
|
};
|
||||||
|
sse.onopen = () => {
|
||||||
|
console.log("EventSource connected");
|
||||||
|
connection_state.innerHTML = "✅ Connected"; // ✅ Check Mark Button
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.error("Sorry, your browser does not support server-sent events...");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStatus(event) {
|
||||||
|
const status = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// print error if present
|
||||||
|
if ("error" in status) {
|
||||||
|
console.error(status.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update "Update DB" button
|
||||||
|
if ("updating_db" in status) {
|
||||||
|
control_update_db.disabled = true;
|
||||||
|
} else {
|
||||||
|
if (control_update_db.disabled) {
|
||||||
|
console.log("Database update done.")
|
||||||
|
}
|
||||||
|
control_update_db.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update play/pause button
|
||||||
|
// TODO: only update DOM if necessary
|
||||||
|
if ("state" in status && status.state !== "play") {
|
||||||
|
control_play_pause.innerHTML = "⏵︎"; // Play
|
||||||
|
} else {
|
||||||
|
control_play_pause.innerHTML = "⏸︎"; // Pause
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("songid" in status) {
|
||||||
|
control_track.dataset.songid = status.songid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update playback time
|
||||||
|
if ("time" in status) {
|
||||||
|
const [elapsed, duration] = status.time.split(":", 2)
|
||||||
|
control_progress.value = elapsed;
|
||||||
|
control_progress.max = duration;
|
||||||
|
// triggers the update of control_time element
|
||||||
|
const e = new Event("input");
|
||||||
|
control_progress.dispatchEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update repeat state
|
||||||
|
if ("repeat" in status) {
|
||||||
|
if (status.repeat === "1") {
|
||||||
|
control_repeat.innerHTML = "🔴 repeat"; // 🔴 Red Circle
|
||||||
|
control_repeat.dataset.state = "on";
|
||||||
|
} else {
|
||||||
|
control_repeat.innerHTML = "🔘 repeat"; // 🔘 Radio Button
|
||||||
|
control_repeat.dataset.state = "off";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update shuffle state
|
||||||
|
if ("random" in status) {
|
||||||
|
if (status.random === "1") {
|
||||||
|
control_shuffle.innerHTML = "🔴 shuffle"; // 🔴 Red Circle
|
||||||
|
control_shuffle.dataset.state = "on";
|
||||||
|
} else {
|
||||||
|
control_shuffle.innerHTML = "🔘 shuffle"; // 🔘 Radio Button
|
||||||
|
control_shuffle.dataset.state = "off";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update crossfade state
|
||||||
|
if ("xfade" in status) {
|
||||||
|
control_xfade.value = status.xfade;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update volume
|
||||||
|
if ("volume" in status) {
|
||||||
|
control_volume.value = status.volume;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCurrentSong(event) {
|
||||||
|
const current_song = JSON.parse(event.data);
|
||||||
|
|
||||||
|
let track;
|
||||||
|
if ("Artist" in current_song && "Title" in current_song) {
|
||||||
|
track = `${current_song.Artist} - ${current_song.Title}`
|
||||||
|
} else {
|
||||||
|
track = current_song.file;
|
||||||
|
}
|
||||||
|
// Only replace if necessary to not interrupt the animation
|
||||||
|
if (control_track.innerHTML !== `<span>${track}</span>`) {
|
||||||
|
control_track.innerHTML = `<span>${track}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQueue(event) {
|
||||||
|
const queue = JSON.parse(event.data);
|
||||||
|
|
||||||
|
console.log(queue);
|
||||||
|
|
||||||
|
const tbody = document.createElement("tbody");
|
||||||
|
queue.forEach(song => {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.dataset.song_id = song.Id;
|
||||||
|
if (control_track.dataset.songid === song.Id) {
|
||||||
|
tr.classList.add("playing");
|
||||||
|
}
|
||||||
|
const pos = document.createElement("td");
|
||||||
|
pos.innerText = song.Pos;
|
||||||
|
const artist = document.createElement("td");
|
||||||
|
if ("Artist" in song) {
|
||||||
|
artist.innerText = song.Artist;
|
||||||
|
}
|
||||||
|
const track = document.createElement("td");
|
||||||
|
if ("Title" in song) {
|
||||||
|
track.innerText = song.Title;
|
||||||
|
} else {
|
||||||
|
track.innerText = song.file;
|
||||||
|
}
|
||||||
|
const album = document.createElement("td");
|
||||||
|
// TODO: Do songs have album info attached to them?
|
||||||
|
album.innerText = "";
|
||||||
|
const length = document.createElement("td");
|
||||||
|
length.innerText = secondsToTrackTime(song.duration);
|
||||||
|
const actions = document.createElement("td");
|
||||||
|
const moveUp = document.createElement("button");
|
||||||
|
moveUp.classList.add("borderless");
|
||||||
|
if (parseInt(song.Pos) !== 0) {
|
||||||
|
moveUp.innerHTML = "🔺"; // 🔺 Red Triangle Pointed Down
|
||||||
|
moveUp.addEventListener("click", event => { moveTrackInQueue(event, -1) });
|
||||||
|
} else {
|
||||||
|
moveUp.innerHTML = " ";
|
||||||
|
}
|
||||||
|
const moveDown = document.createElement("button");
|
||||||
|
moveDown.classList.add("borderless");
|
||||||
|
if (parseInt(song.Pos) !== queue.length - 1) {
|
||||||
|
moveDown.innerHTML = "🔻"; // 🔻 Red Triangle Pointed Up
|
||||||
|
moveDown.addEventListener("click", event => {moveTrackInQueue(event, 1)});
|
||||||
|
} else {
|
||||||
|
moveDown.innerHTML = " ";
|
||||||
|
}
|
||||||
|
const remove = document.createElement("button");
|
||||||
|
remove.classList.add("borderless");
|
||||||
|
remove.innerHTML = "❌"; // ❌ Cross mark
|
||||||
|
remove.addEventListener("click", removeTrackFromQueue);
|
||||||
|
actions.appendChild(moveUp);
|
||||||
|
actions.appendChild(moveDown);
|
||||||
|
actions.appendChild(remove);
|
||||||
|
tr.appendChild(pos);
|
||||||
|
tr.appendChild(artist);
|
||||||
|
tr.appendChild(track);
|
||||||
|
tr.appendChild(album);
|
||||||
|
tr.appendChild(length);
|
||||||
|
tr.appendChild(actions);
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
const currentQueue = document.querySelector("#queue tbody")
|
||||||
|
// only update queue if necessary to not interrupt user interaction
|
||||||
|
if (currentQueue.innerHTML !== tbody.innerHTML) {
|
||||||
|
console.log("Updating queue")
|
||||||
|
currentQueue.outerHTML = tbody.outerHTML;
|
||||||
|
}
|
||||||
|
}
|
437
static/style.css
Normal file
437
static/style.css
Normal file
|
@ -0,0 +1,437 @@
|
||||||
|
:root {
|
||||||
|
--ribbon-width: 160px;
|
||||||
|
--ribbon-height: 80px;
|
||||||
|
|
||||||
|
--background-color: #041936;
|
||||||
|
--text-color: #bbb;
|
||||||
|
|
||||||
|
--input-background-color: #28374a;
|
||||||
|
--input-border-light: #545454;
|
||||||
|
--input-border-dark: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #################### */
|
||||||
|
/* #### structure ##### */
|
||||||
|
/* #################### */
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-columns: 1fr;
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
grid-template-rows: var(--ribbon-height) 1fr 1fr;
|
||||||
|
gap: 0 0;
|
||||||
|
grid-template-areas: "nav nav" "queue queue" "browser result" "footer footer";
|
||||||
|
}
|
||||||
|
|
||||||
|
#queue {
|
||||||
|
grid-area: queue;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav {
|
||||||
|
grid-area: nav;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav > div {
|
||||||
|
width: var(--ribbon-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
#result {
|
||||||
|
grid-area: result;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#browser {
|
||||||
|
grid-area: browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
main footer {
|
||||||
|
grid-area: footer;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-admin {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sanic-logo {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sanic-logo > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sanic-logo img {
|
||||||
|
max-width: 75%;
|
||||||
|
max-height: 75%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spaced {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #################### */
|
||||||
|
/* ### pretty stuff ### */
|
||||||
|
/* #################### */
|
||||||
|
|
||||||
|
#control-progress {
|
||||||
|
width: var(--ribbon-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-volume {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav > div.wide {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav > div.wide div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable arrows in input */
|
||||||
|
|
||||||
|
/* Chrome, Safari, Edge, Opera */
|
||||||
|
#control-xfade::-webkit-outer-spin-button,
|
||||||
|
#control-xfade::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
#control-xfade[type=number] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-xfade {
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-previous,
|
||||||
|
#control-play-pause,
|
||||||
|
#control-stop,
|
||||||
|
#control-next {
|
||||||
|
width: 2.5em;
|
||||||
|
height: 2.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#control-track {
|
||||||
|
transform: translateX(100%);
|
||||||
|
-moz-transform: translateX(100%);
|
||||||
|
-webkit-transform: translateX(100%);
|
||||||
|
animation: scroll-left 20s linear infinite;
|
||||||
|
-moz-animation: scroll-left 2s linear infinite;
|
||||||
|
-webkit-animation: scroll-left 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll-left {
|
||||||
|
0% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
-moz-transform: translateX(100%);
|
||||||
|
-webkit-transform: translateX(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
-moz-transform: translateX(-100%);
|
||||||
|
-webkit-transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-keyframes scroll-left {
|
||||||
|
0% {
|
||||||
|
-moz-transform: translateX(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-moz-transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes scroll-left {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translateX(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background-color: #09101d;
|
||||||
|
color: var(--text-color);
|
||||||
|
scrollbar-color: #490b00 #09101d; /* only in firefox: https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color */
|
||||||
|
font-weight: normal;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border-top-color: var(--input-border-light);
|
||||||
|
border-right-color: var(--input-border-dark);
|
||||||
|
border-bottom-color: var(--input-border-dark);
|
||||||
|
border-left-color: var(--input-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
button[disabled] {
|
||||||
|
color: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
button[disabled] .loader {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button .loader {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
width: 10pt;
|
||||||
|
height: 10pt;
|
||||||
|
border: 3px solid var(--text-color);
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: rotation 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotation {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* borderless button used in queue */
|
||||||
|
.borderless {
|
||||||
|
border: none;
|
||||||
|
background-color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid black;
|
||||||
|
border-right-color: var(--input-border-light);
|
||||||
|
border-bottom-color: var(--input-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: var(--ribbon-width);
|
||||||
|
font-size: 10pt;
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid black;
|
||||||
|
border-right-color: var(--input-border-light);
|
||||||
|
border-bottom-color: var(--input-border-light);
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee > * {
|
||||||
|
-webkit-animation: marquee 10s linear infinite both alternate;
|
||||||
|
animation: marquee 10s linear infinite both alternate;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes marquee {
|
||||||
|
to {
|
||||||
|
transform: translateX(min(100cqw - 100%, 0px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes marquee {
|
||||||
|
to {
|
||||||
|
transform: translateX(min(100cqw - 100%, 0px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav {
|
||||||
|
padding: 5px;
|
||||||
|
background: linear-gradient(0deg, rgba(3,7,11,1) 0%, rgba(14,27,43,1) 4%, rgba(41,55,74,1) 6%, rgba(18,35,56,1) 94%, rgba(40,68,104,1) 96%, rgba(168,182,200,1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav > div {
|
||||||
|
border-right: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background: #0F1D2F linear-gradient(0deg, rgba(15, 29, 47, 1) 0%, rgba(15, 29, 47, 1) 50%, rgba(7, 14, 23, 1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 2px 2px 2px 14px;
|
||||||
|
border: solid #1c2c1a;
|
||||||
|
border-width: 0 1px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* show and hide action buttons on hover */
|
||||||
|
tbody tr td button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover td button {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fixed width for action buttons in queue so it doesn't change size when hovering */
|
||||||
|
tbody tr td:last-of-type {
|
||||||
|
min-width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td.actions {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#queue {
|
||||||
|
border-bottom: 4px ridge #3a506b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make arrow for currently playing song look nice */
|
||||||
|
|
||||||
|
#queue table tr.playing td:first-of-type::before {
|
||||||
|
content: '\2BC8'; /* ⯈ Black Medium Right-Pointing Triangle Centred */
|
||||||
|
}
|
||||||
|
|
||||||
|
#queue table tr td:first-of-type {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* align times */
|
||||||
|
|
||||||
|
#queue table tr td:nth-last-of-type(2) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-child(odd) td {
|
||||||
|
background-color: #1e1f1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-child(even) td {
|
||||||
|
background-color: #171812;
|
||||||
|
}
|
||||||
|
|
||||||
|
#queue table tr:nth-child(odd).playing td,
|
||||||
|
#queue table tr:nth-child(even).playing td {
|
||||||
|
background-color: #490b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:hover td {
|
||||||
|
background-color: #354158;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs a {
|
||||||
|
width: 50%;
|
||||||
|
padding: 3pt;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid var(--input-border-light);
|
||||||
|
border-top-left-radius: 5pt;
|
||||||
|
border-top-right-radius: 5pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs a.active {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: var(--text-color);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#browser {
|
||||||
|
background-color: #171812;
|
||||||
|
border-right: 4px ridge #3a506b;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-playlist-list {
|
||||||
|
font-size: 12pt;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid black;
|
||||||
|
border-right-color: var(--input-border-light);
|
||||||
|
border-bottom-color: var(--input-border-light);
|
||||||
|
scrollbar-color: #490b00 #09101d; /* only in firefox: https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color */
|
||||||
|
}
|
||||||
|
|
||||||
|
footer svg {
|
||||||
|
color: var(--text-color);
|
||||||
|
width: 12pt;
|
||||||
|
height: 12pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*dialog {*/
|
||||||
|
/* position: fixed;*/
|
||||||
|
/* left: 50%;*/
|
||||||
|
/* top: 50%;*/
|
||||||
|
/* transform: translate(-50%, -50%);*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog .close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1pt;
|
||||||
|
right: 1pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-search-pattern {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search:first-child {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
71
static/test.html
Normal file
71
static/test.html
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>API test</title>
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48 64x64" type="image/png" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<textarea id="log"></textarea>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Create WebSocket connection.
|
||||||
|
const socket = new WebSocket("ws://localhost:1323/ws");
|
||||||
|
|
||||||
|
// Connection opened
|
||||||
|
socket.addEventListener("open", (event) => {
|
||||||
|
socket.send("Hello Server!");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for messages
|
||||||
|
socket.addEventListener("message", (event) => {
|
||||||
|
console.log("server: ", event.data);
|
||||||
|
|
||||||
|
if (typeof event.data === "object" && "mpd_status" in event.data) {
|
||||||
|
// {"consume":"0","mixrampdb":"0","partition":"default","playlist":"1","playlistlength":"0","random":"0","repeat":"0","single":"0","state":"stop","volume":"100"}
|
||||||
|
|
||||||
|
// random
|
||||||
|
if ("random" in event.data) {
|
||||||
|
const randomToggle = document.getElementById("randomToggle");
|
||||||
|
randomToggle.checked = event.data.random === "0";
|
||||||
|
}
|
||||||
|
// repeat
|
||||||
|
if ("repeat" in event.data) {
|
||||||
|
const repeatToggle = document.getElementById("repeatToggle");
|
||||||
|
repeatToggle.checked = event.data.repeat === "0";
|
||||||
|
}
|
||||||
|
// state
|
||||||
|
if ("state" in event.data) {
|
||||||
|
const playPauseButton = document.getElementById("playPauseButton");
|
||||||
|
switch (event.data.state) {
|
||||||
|
case "play":
|
||||||
|
playPauseButton.value = "||"
|
||||||
|
break;
|
||||||
|
case "stop":
|
||||||
|
case "pause":
|
||||||
|
playPauseButton.value = ">"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// volume
|
||||||
|
if ("volume" in event.data) {
|
||||||
|
const volumeInput = document.getElementById("volumeInput");
|
||||||
|
volumeInput.value = event.data.volume
|
||||||
|
}
|
||||||
|
// current song
|
||||||
|
if ("elapsed" in event.data && "duration" in event.data) {
|
||||||
|
const seekTrackInput = document.getElementById("trackSeekInput");
|
||||||
|
seekTrackInput.max = event.data.duration;
|
||||||
|
seekTrackInput.value = event.data.elapsed;
|
||||||
|
const currentSongTimeText = document.getElementById("currentSongTimeText");
|
||||||
|
currentSongTimeText.text = event.data.elapsed + " | " + event.data.duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// window.setInterval(() => {
|
||||||
|
// socket.send("#status")
|
||||||
|
// }, 1000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,91 +1,150 @@
|
||||||
/* https://iamkate.com/code/tree-views/ */
|
/* https://codepen.io/defims/pen/DBaVXM */
|
||||||
|
|
||||||
/* Custom properties */
|
ul {
|
||||||
|
float: left;
|
||||||
.tree {
|
clear: left;
|
||||||
--spacing : 1.0rem;
|
margin-left: .25em;
|
||||||
--radius : 10px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Padding */
|
ul:before {
|
||||||
|
content:"";
|
||||||
.tree li {
|
position: absolute;
|
||||||
display : block;
|
z-index: 1;
|
||||||
position : relative;
|
top:.25em;
|
||||||
padding-left : calc(2 * var(--spacing) - var(--radius) - 2px + 10px);
|
right:auto;
|
||||||
|
bottom:0;
|
||||||
|
left: 1.75em;
|
||||||
|
margin: auto;
|
||||||
|
/* border-right: dotted white .1em; */
|
||||||
|
width: 0;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree ul {
|
ul:after {
|
||||||
margin-left : calc(var(--radius) - var(--spacing));
|
content: "-";
|
||||||
padding-left : 0;
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 0;
|
||||||
|
left: -.5em;
|
||||||
|
margin-left: .65em;
|
||||||
|
margin-top: .15em;
|
||||||
|
padding: 0;
|
||||||
|
width: .8em;
|
||||||
|
height: .8em;
|
||||||
|
text-align: center;
|
||||||
|
line-height: .7em;
|
||||||
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Vertical lines */
|
ul > li {
|
||||||
|
display: block;
|
||||||
.tree ul li {
|
position: relative;
|
||||||
border-left : 2px solid #ddd;
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
right:auto;
|
||||||
|
padding-left: 1em;
|
||||||
|
width:auto;
|
||||||
|
text-align: center;
|
||||||
|
color:white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree ul li:last-child {
|
ul > li > input[type=checkbox] {
|
||||||
border-color : transparent;
|
display:block;
|
||||||
|
position: absolute;
|
||||||
|
float: left;
|
||||||
|
z-index: 4;
|
||||||
|
margin: 0 0 0 -1em;
|
||||||
|
padding: 0;
|
||||||
|
width:1em;
|
||||||
|
height: 2em;
|
||||||
|
font-size: 1em;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Horizontal lines */
|
ul > li > input[type=checkbox]:not(:checked)~ul:before {
|
||||||
|
display: none;
|
||||||
.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 */
|
ul > li > input[type=checkbox]:not(:checked)~ul:after {
|
||||||
|
content: "+"
|
||||||
.tree summary {
|
|
||||||
display : block;
|
|
||||||
cursor : pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree summary::marker,
|
ul > li > input[type=checkbox]:not(:checked)~ul * {
|
||||||
.tree summary::-webkit-details-marker {
|
display: none;
|
||||||
display : none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree summary:focus {
|
ul > li > span {
|
||||||
outline : none;
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
z-index: 3;
|
||||||
|
margin-left: .25em;
|
||||||
|
padding-left: .25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree summary:focus-visible {
|
ul > li > span:after {
|
||||||
outline : 1px dotted #000;
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left:-1em;
|
||||||
|
top:0;
|
||||||
|
bottom:0;
|
||||||
|
margin: auto .25em auto .25em;
|
||||||
|
/* border-top: dotted white .1em; */
|
||||||
|
width: .75em;
|
||||||
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Markers */
|
ul > li:last-child:before {
|
||||||
|
content: ""; display: block; position: absolute; z-index: 2;
|
||||||
.tree li::after,
|
top:1em; left:0; bottom:-.25em;
|
||||||
.tree summary::before {
|
width:.75em; height:auto;
|
||||||
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 {
|
||||||
|
position: relative; font-family: "Georgia";
|
||||||
.tree summary::before {
|
|
||||||
z-index : 1;
|
|
||||||
background : #696 url('img/expand-collapse.svg') 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree details[open] > summary::before {
|
#tree:before {
|
||||||
background-position : calc(-2 * var(--radius)) 0;
|
left:.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tree:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* decoration */
|
||||||
|
ul, ul > li:last-child:before {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul:after {
|
||||||
|
background: linear-gradient(135deg, rgba(255,255,255,1), rgba(195,186,170,1));
|
||||||
|
color: black;
|
||||||
|
border:solid gray 1px;
|
||||||
|
border-radius: .1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li > span {
|
||||||
|
border-radius: .25em;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li > input[type=checkbox]~span:before {
|
||||||
|
content:""; display: inline-block;
|
||||||
|
margin: 0 .25em 0 0;
|
||||||
|
width:1em; height: 1em; ;line-height: 1em;
|
||||||
|
content: '\1F4C1';
|
||||||
|
background-repeat:no-repeat;
|
||||||
|
background-size:contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li > input[type=checkbox]:checked~span:before {
|
||||||
|
content: '\1F4C2';
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue