things
This commit is contained in:
parent
7180ddcfbc
commit
fd4eb5b3e8
18 changed files with 195 additions and 272 deletions
53
aur/PKGBUILD
53
aur/PKGBUILD
|
|
@ -1,53 +0,0 @@
|
||||||
# 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"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
[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
|
|
||||||
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
u sanic - "chaos music control" /run/sanic /usr/bin/nologin
|
|
||||||
g sanic - -
|
|
||||||
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
d /etc/sanic 0750 sanic sanic
|
|
||||||
d /run/sanic 0750 sanic sanic
|
|
||||||
|
|
||||||
114
flake.nix
114
flake.nix
|
|
@ -37,119 +37,7 @@
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
packages.default = sanic;
|
packages.default = sanic;
|
||||||
nixosModules.default = { config, lib, pkgs, options, ... }:
|
nixosModules.default = import ./option.nix;
|
||||||
let
|
|
||||||
cfg = config.services.sanic;
|
|
||||||
configFile = pkgs.writeText "config.ini" (pkgs.lib.generators.toINI {} cfg);
|
|
||||||
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 = 8443;
|
|
||||||
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 = 8080;
|
|
||||||
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;
|
|
||||||
#};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -6,6 +6,7 @@ require (
|
||||||
github.com/fhs/gompd/v2 v2.3.0
|
github.com/fhs/gompd/v2 v2.3.0
|
||||||
github.com/labstack/echo-contrib v0.17.1
|
github.com/labstack/echo-contrib v0.17.1
|
||||||
github.com/labstack/echo/v4 v4.12.0
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
|
github.com/tdewolff/minify/v2 v2.24.0
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -21,11 +22,12 @@ require (
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.55.0 // indirect
|
github.com/prometheus/common v0.55.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/tdewolff/parse/v2 v2.8.3 // 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.26.0 // indirect
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
golang.org/x/net v0.28.0 // indirect
|
golang.org/x/net v0.28.0 // indirect
|
||||||
golang.org/x/sys v0.23.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
golang.org/x/time v0.6.0 // indirect
|
golang.org/x/time v0.6.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
|
|
|
||||||
10
go.sum
10
go.sum
|
|
@ -35,6 +35,12 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tdewolff/minify/v2 v2.24.0 h1:m6j8VXvgUtmkavubzHbaNTXi9tw3hjIMZbdc57SRdvI=
|
||||||
|
github.com/tdewolff/minify/v2 v2.24.0/go.mod h1:uqtSu3w0+anqk4ofcsuLPZ8tV8yAZL1r/ILWYYl2j3c=
|
||||||
|
github.com/tdewolff/parse/v2 v2.8.3 h1:5VbvtJ83cfb289A1HzRA9sf02iT8YyUwN84ezjkdY1I=
|
||||||
|
github.com/tdewolff/parse/v2 v2.8.3/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||||
|
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||||
|
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
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.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
|
@ -45,8 +51,8 @@ golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
golang.org/x/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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
|
|
|
||||||
13
mpd.go
13
mpd.go
|
|
@ -1,12 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fhs/gompd/v2/mpd"
|
"net/http"
|
||||||
"github.com/labstack/echo/v4"
|
"strconv"
|
||||||
"net/http"
|
"time"
|
||||||
"strconv"
|
|
||||||
"time"
|
"github.com/fhs/gompd/v2/mpd"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MPD API calls
|
// MPD API calls
|
||||||
|
|
|
||||||
114
option.nix
Normal file
114
option.nix
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
{ config, lib, pkgs, options, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.sanic;
|
||||||
|
configFile = pkgs.writeText "config.ini" (pkgs.lib.generators.toINI {} cfg);
|
||||||
|
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 = 8443;
|
||||||
|
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 = 8080;
|
||||||
|
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;
|
||||||
|
#};
|
||||||
|
}
|
||||||
|
|
@ -26,4 +26,3 @@ TimeoutStartSec=900
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target default.target
|
WantedBy=multi-user.target default.target
|
||||||
|
|
||||||
|
|
|
||||||
27
server.go
27
server.go
|
|
@ -1,24 +1,29 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"embed"
|
||||||
"github.com/labstack/echo-contrib/echoprometheus"
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
"net/http"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"os"
|
||||||
"gopkg.in/ini.v1"
|
"os/exec"
|
||||||
"net/http"
|
|
||||||
"os"
|
"github.com/labstack/echo-contrib/echoprometheus"
|
||||||
"os/exec"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds the configuration for the mpd connection and for the web server.
|
//go:embed static/*
|
||||||
|
var staticFS embed.FS
|
||||||
|
|
||||||
|
// Config holds the configuration for the mpd backend connection and for the web server.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
MPD struct {
|
BACKEND struct {
|
||||||
Hostname string `ini:"hostname"`
|
Hostname string `ini:"hostname"`
|
||||||
Port int `ini:"port"`
|
Port int `ini:"port"`
|
||||||
Username string `ini:"username"`
|
Username string `ini:"username"`
|
||||||
Password string `ini:"password"`
|
Password string `ini:"password"`
|
||||||
} `ini:"mpd"`
|
} `ini:"backend"`
|
||||||
UI struct {
|
UI struct {
|
||||||
Hostname string `ini:"hostname"`
|
Hostname string `ini:"hostname"`
|
||||||
Port int `ini:"port"`
|
Port int `ini:"port"`
|
||||||
|
|
|
||||||
30
services.nix
30
services.nix
|
|
@ -1,30 +0,0 @@
|
||||||
{ 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" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
15
sse.go
15
sse.go
|
|
@ -1,13 +1,14 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fhs/gompd/v2/mpd"
|
"io"
|
||||||
"github.com/labstack/echo/v4"
|
"time"
|
||||||
"io"
|
|
||||||
"time"
|
"github.com/fhs/gompd/v2/mpd"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event represents Server-Sent Event.
|
// Event represents Server-Sent Event.
|
||||||
|
|
|
||||||
437
static/css/style.css
Normal file
437
static/css/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;
|
||||||
|
}
|
||||||
150
static/css/treeview.css
Normal file
150
static/css/treeview.css
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
/* https://codepen.io/defims/pen/DBaVXM */
|
||||||
|
|
||||||
|
ul {
|
||||||
|
float: left;
|
||||||
|
clear: left;
|
||||||
|
margin-left: .25em;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul:before {
|
||||||
|
content:"";
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top:.25em;
|
||||||
|
right:auto;
|
||||||
|
bottom:0;
|
||||||
|
left: 1.75em;
|
||||||
|
margin: auto;
|
||||||
|
/* border-right: dotted white .1em; */
|
||||||
|
width: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul:after {
|
||||||
|
content: "-";
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
right:auto;
|
||||||
|
padding-left: 1em;
|
||||||
|
width:auto;
|
||||||
|
text-align: center;
|
||||||
|
color:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li > input[type=checkbox] {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li > input[type=checkbox]:not(:checked)~ul:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li > input[type=checkbox]:not(:checked)~ul:after {
|
||||||
|
content: "+"
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li > input[type=checkbox]:not(:checked)~ul * {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li > span {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
z-index: 3;
|
||||||
|
margin-left: .25em;
|
||||||
|
padding-left: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li > span:after {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li:last-child:before {
|
||||||
|
content: ""; display: block; position: absolute; z-index: 2;
|
||||||
|
top:1em; left:0; bottom:-.25em;
|
||||||
|
width:.75em; height:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tree {
|
||||||
|
position: relative; font-family: "Georgia";
|
||||||
|
}
|
||||||
|
|
||||||
|
#tree:before {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,38 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" xmlns="http://www.w3.org/1999/html">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<meta name="application-name" content="Sanic">
|
||||||
|
|
||||||
|
<!-- seo data -->
|
||||||
|
<meta name="description" lang="en" content="chaos music control inspired by relaxx player">
|
||||||
|
<meta name="keywords" lang="en" content="music, playlist, mpd, ui">
|
||||||
|
<meta name="author" content="XenGi">
|
||||||
|
<link rel="canonical" href="https://gitlab.com/XenGi/sanic">
|
||||||
|
|
||||||
|
<!-- social media -->
|
||||||
|
<meta property="og:title" content="Sanic">
|
||||||
|
<meta property="og:description" content="chaos music control inspired by relaxx player">
|
||||||
|
<meta property="og:url" content="https://gitlab.com/XenGi/sanic">
|
||||||
|
<meta property="og:image" content="https://gitlab.com/XenGi/sanic/-/raw/main/static/img/sanic-logo.webp?ref_type=heads&inline=false">
|
||||||
|
<meta property="og:image:alt" content="Crappy picture of sonic the hedgehog">
|
||||||
|
<meta name="twitter:title" content="Sanic">
|
||||||
|
<meta name="twitter:description" content="chaos music control inspired by relaxx player">
|
||||||
|
<meta name="twitter:url" content="https://gitlab.com/XenGi/sanic">
|
||||||
|
<meta name="twitter:image:src" content="https://gitlab.com/XenGi/sanic/-/raw/main/static/img/sanic-logo.webp?ref_type=heads&inline=false">
|
||||||
|
<meta name="twitter:image:alt" content="Crappy picture of sonic the hedgehog">
|
||||||
|
|
||||||
|
<!-- icons and color -->
|
||||||
|
<meta name="theme-color" content="#000000">
|
||||||
|
<meta name="theme-scheme" content="dark">
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico">
|
||||||
|
|
||||||
<title>Sanic</title>
|
<title>Sanic</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
<link rel="stylesheet" href="treeview.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="icon" href="favicon.ico" sizes="16x16 32x32 48x48 64x64" type="image/png">
|
<link rel="stylesheet" href="css/treeview.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -17,7 +44,7 @@
|
||||||
<input type="text" id="control-playlist-name" name="playlist-name" autofocus>
|
<input type="text" id="control-playlist-name" name="playlist-name" autofocus>
|
||||||
<button>Save</button>
|
<button>Save</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog><!--/#save-playlist-->
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
|
|
@ -36,7 +63,7 @@
|
||||||
<div class="spaced">
|
<div class="spaced">
|
||||||
<label for="control-progress"></label>
|
<label for="control-progress"></label>
|
||||||
<input type="range" id="control-progress" name="progress" min="0" step="1" />
|
<input type="range" id="control-progress" name="progress" min="0" step="1" />
|
||||||
</div>
|
</div><!--/.spaced-->
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="spaced">
|
<div class="spaced">
|
||||||
|
|
@ -63,20 +90,20 @@
|
||||||
<!--<input type="text" id="control-track" name="track" disabled="disabled" />-->
|
<!--<input type="text" id="control-track" name="track" disabled="disabled" />-->
|
||||||
<div class="marquee" id="control-track" data-songid="">
|
<div class="marquee" id="control-track" data-songid="">
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div><!--/#control-track /.marquee-->
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="control-time">Time:</label>
|
<label for="control-time">Time:</label>
|
||||||
<input type="text" id="control-time" name="time" value="00:00:00/00:00:00" disabled="disabled" />
|
<input type="text" id="control-time" name="time" value="00:00:00/00:00:00" disabled="disabled" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div><!--/.wide-->
|
||||||
<div id="sanic-logo">
|
<div id="sanic-logo">
|
||||||
<div><!-- TODO: try to remove this div -->
|
<div><!-- TODO: try to remove this div -->
|
||||||
<img alt="sanic logo" src="img/sanic-logo.webp" />
|
<img alt="sanic logo" src="img/sanic-logo.webp" />
|
||||||
Sanic © 2023
|
Sanic © 2023
|
||||||
</div>
|
</div>
|
||||||
</div><!--/#sanic-logo-->
|
</div><!--/#sanic-logo-->
|
||||||
</div>
|
</div><!--/#nav-->
|
||||||
<div id="queue">
|
<div id="queue">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -179,7 +206,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul><!--/#tree-->
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
actions
|
actions
|
||||||
|
|
|
||||||
358
static/js/index.js
Normal file
358
static/js/index.js
Normal file
|
|
@ -0,0 +1,358 @@
|
||||||
|
// 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}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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/js/sse.js
Normal file
178
static/js/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue