feat(jukebox): use mpv over ipc as a player backend
This commit is contained in:
2
.github/workflows/nightly-release.yaml
vendored
2
.github/workflows/nightly-release.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update -qq
|
sudo apt update -qq
|
||||||
sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg libasound-dev zlib1g-dev
|
sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev
|
||||||
- name: Lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update -qq
|
sudo apt update -qq
|
||||||
sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg libasound-dev zlib1g-dev
|
sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev
|
||||||
- name: Lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update -qq
|
sudo apt update -qq
|
||||||
sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg libasound-dev zlib1g-dev
|
sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev
|
||||||
- name: Lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ RUN apk add -U --no-cache \
|
|||||||
git \
|
git \
|
||||||
sqlite \
|
sqlite \
|
||||||
taglib-dev \
|
taglib-dev \
|
||||||
alsa-lib-dev \
|
|
||||||
zlib-dev \
|
zlib-dev \
|
||||||
go
|
go
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@@ -15,10 +14,11 @@ RUN go mod download
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN GOOS=linux go build -o gonic cmd/gonic/gonic.go
|
RUN GOOS=linux go build -o gonic cmd/gonic/gonic.go
|
||||||
|
|
||||||
FROM alpine:3.15
|
FROM alpine:3.16
|
||||||
LABEL org.opencontainers.image.source https://github.com/sentriz/gonic
|
LABEL org.opencontainers.image.source https://github.com/sentriz/gonic
|
||||||
RUN apk add -U --no-cache \
|
RUN apk add -U --no-cache \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
|
mpv \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
tini
|
tini
|
||||||
|
|||||||
@@ -5,6 +5,5 @@ RUN apk add -U --no-cache \
|
|||||||
git \
|
git \
|
||||||
sqlite \
|
sqlite \
|
||||||
taglib-dev \
|
taglib-dev \
|
||||||
alsa-lib-dev \
|
|
||||||
zlib-dev
|
zlib-dev
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ RUN apk add -U --no-cache \
|
|||||||
git \
|
git \
|
||||||
sqlite \
|
sqlite \
|
||||||
taglib-dev \
|
taglib-dev \
|
||||||
alsa-lib-dev \
|
|
||||||
zlib-dev
|
zlib-dev
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -15,9 +14,10 @@ RUN --mount=type=cache,target=/go/pkg/mod \
|
|||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
GOOS=linux go build -o gonic cmd/gonic/gonic.go
|
GOOS=linux go build -o gonic cmd/gonic/gonic.go
|
||||||
|
|
||||||
FROM alpine:3.15
|
FROM alpine:3.16
|
||||||
RUN apk add -U --no-cache \
|
RUN apk add -U --no-cache \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
|
mpv \
|
||||||
ca-certificates
|
ca-certificates
|
||||||
COPY --from=builder \
|
COPY --from=builder \
|
||||||
/usr/lib/libgcc_s.so.1 \
|
/usr/lib/libgcc_s.so.1 \
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func main() {
|
|||||||
g.Add(server.StartScanWatcher())
|
g.Add(server.StartScanWatcher())
|
||||||
}
|
}
|
||||||
if *confJukeboxEnabled {
|
if *confJukeboxEnabled {
|
||||||
g.Add(server.StartJukebox())
|
g.Add(server.StartJukebox(nil))
|
||||||
}
|
}
|
||||||
if *confPodcastPurgeAgeDays > 0 {
|
if *confPodcastPurgeAgeDays > 0 {
|
||||||
g.Add(server.StartPodcastPurger(time.Duration(*confPodcastPurgeAgeDays) * 24 * time.Hour))
|
g.Add(server.StartPodcastPurger(time.Duration(*confPodcastPurgeAgeDays) * 24 * time.Hour))
|
||||||
|
|||||||
13
go.mod
13
go.mod
@@ -4,9 +4,9 @@ go 1.19
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/sprig v2.22.0+incompatible
|
github.com/Masterminds/sprig v2.22.0+incompatible
|
||||||
|
github.com/dexterlb/mpvipc v0.0.0-20210824102722-5d27ef06b6c3
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/faiface/beep v1.1.0
|
|
||||||
github.com/fsnotify/fsnotify v1.6.0
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
@@ -16,6 +16,7 @@ require (
|
|||||||
github.com/josephburnett/jd v1.5.2
|
github.com/josephburnett/jd v1.5.2
|
||||||
github.com/matryer/is v1.4.0
|
github.com/matryer/is v1.4.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.16
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/mmcdole/gofeed v1.1.3
|
github.com/mmcdole/gofeed v1.1.3
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd
|
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd
|
||||||
@@ -24,6 +25,7 @@ require (
|
|||||||
github.com/peterbourgon/ff v1.7.1
|
github.com/peterbourgon/ff v1.7.1
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
||||||
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981
|
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981
|
||||||
|
golang.org/x/exp v0.0.0-20221114191408-850992195362
|
||||||
gopkg.in/gormigrate.v1 v1.6.0
|
gopkg.in/gormigrate.v1 v1.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,10 +38,7 @@ require (
|
|||||||
github.com/go-openapi/swag v0.21.1 // indirect
|
github.com/go-openapi/swag v0.21.1 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
|
|
||||||
github.com/hajimehoshi/oto v1.0.1 // indirect
|
|
||||||
github.com/huandu/xstrings v1.3.3 // indirect
|
github.com/huandu/xstrings v1.3.3 // indirect
|
||||||
github.com/icza/bitio v1.1.0 // indirect
|
|
||||||
github.com/imdario/mergo v0.3.13 // indirect
|
github.com/imdario/mergo v0.3.13 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.2 // indirect
|
github.com/jinzhu/now v1.1.2 // indirect
|
||||||
@@ -47,21 +46,17 @@ require (
|
|||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/lib/pq v1.3.0 // indirect
|
github.com/lib/pq v1.3.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mewkiz/flac v1.0.7 // indirect
|
|
||||||
github.com/mewkiz/pkg v0.0.0-20220820102221-bbbca16e2a6c // indirect
|
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/mmcdole/goxpp v0.0.0-20200921145534-2f3784f67354 // indirect
|
github.com/mmcdole/goxpp v0.0.0-20200921145534-2f3784f67354 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/stretchr/testify v1.7.0 // indirect
|
github.com/stretchr/testify v1.7.0 // indirect
|
||||||
golang.org/x/crypto v0.2.0 // indirect
|
golang.org/x/crypto v0.2.0 // indirect
|
||||||
golang.org/x/exp/shiny v0.0.0-20221114191408-850992195362 // indirect
|
|
||||||
golang.org/x/image v0.1.0 // indirect
|
golang.org/x/image v0.1.0 // indirect
|
||||||
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect
|
|
||||||
golang.org/x/net v0.2.0 // indirect
|
golang.org/x/net v0.2.0 // indirect
|
||||||
golang.org/x/sys v0.2.0 // indirect
|
golang.org/x/sys v0.2.0 // indirect
|
||||||
golang.org/x/text v0.4.0 // indirect
|
golang.org/x/text v0.4.0 // indirect
|
||||||
|
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
56
go.sum
56
go.sum
@@ -1,6 +1,5 @@
|
|||||||
cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
@@ -15,28 +14,22 @@ github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x0
|
|||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
|
github.com/dexterlb/mpvipc v0.0.0-20210824102722-5d27ef06b6c3 h1:uIJS8tUx2f4rciUwL0wEHuwVI2tH9rQHUMnm4gHuhXs=
|
||||||
|
github.com/dexterlb/mpvipc v0.0.0-20210824102722-5d27ef06b6c3/go.mod h1:CXCwawNJCtFDip7gvbaQVgw0cGjldpyHDIp7oA5prOg=
|
||||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||||
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
|
|
||||||
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
|
||||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
|
||||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
|
||||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
|
||||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
@@ -63,25 +56,10 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
|
|||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
|
||||||
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
|
|
||||||
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
|
|
||||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
|
||||||
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
|
||||||
github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4=
|
|
||||||
github.com/hajimehoshi/oto v1.0.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
|
||||||
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
|
|
||||||
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
|
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
|
||||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
|
||||||
github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0=
|
|
||||||
github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
|
||||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
|
|
||||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
|
||||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||||
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
|
|
||||||
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
|
|
||||||
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||||
github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414 h1:JkXdZo2OKW1t+GcTx5eb1kD2qW5lt1CDLrL2Ep9t+j4=
|
github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414 h1:JkXdZo2OKW1t+GcTx5eb1kD2qW5lt1CDLrL2Ep9t+j4=
|
||||||
github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||||
@@ -101,7 +79,6 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
|||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/jszwec/csvutil v1.5.1/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
@@ -111,7 +88,6 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
|||||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
@@ -119,19 +95,15 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
|
|||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
|
||||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
|
|
||||||
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
|
|
||||||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
|
|
||||||
github.com/mewkiz/pkg v0.0.0-20220820102221-bbbca16e2a6c h1:6AzCfQNCql3Of8ee1JY6dufssFnBWJYuCVrGcES84AA=
|
|
||||||
github.com/mewkiz/pkg v0.0.0-20220820102221-bbbca16e2a6c/go.mod h1:J/rDzvIiwiVpv72OEP8aJFxLXjGpUdviIIeqJPLIctA=
|
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
github.com/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o=
|
github.com/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o=
|
||||||
@@ -158,9 +130,6 @@ github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwU
|
|||||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||||
github.com/peterbourgon/ff v1.7.1 h1:xt1lxTG+Nr2+tFtysY7abFgPoH3Lug8CwYJMOmJRXhk=
|
github.com/peterbourgon/ff v1.7.1 h1:xt1lxTG+Nr2+tFtysY7abFgPoH3Lug8CwYJMOmJRXhk=
|
||||||
github.com/peterbourgon/ff v1.7.1/go.mod h1:fYI5YA+3RDqQRExmFbHnBjEeWzh9TrS8rnRpEq7XIg0=
|
github.com/peterbourgon/ff v1.7.1/go.mod h1:fYI5YA+3RDqQRExmFbHnBjEeWzh9TrS8rnRpEq7XIg0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
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/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||||
@@ -183,21 +152,14 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
|
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
|
||||||
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20221114191408-850992195362 h1:NoHlPRbyl1VFI6FjwHtPQCN7wAMXI6cKcqrmXhOOfBQ=
|
||||||
golang.org/x/exp/shiny v0.0.0-20221114191408-850992195362 h1:klJAUGTRrnTvp2+ongrNqLxrl/415DPs2iR9xn/k0ME=
|
golang.org/x/exp v0.0.0-20221114191408-850992195362/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||||
golang.org/x/exp/shiny v0.0.0-20221114191408-850992195362/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8=
|
|
||||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
|
golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
|
||||||
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
|
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
|
||||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
|
||||||
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 h1:aIu0lKmfdgtn2uTj7JI2oN4TUrQvgB+wzTPO23bCKt8=
|
|
||||||
golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -211,16 +173,12 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
|||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||||
@@ -246,6 +204,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
|
|||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
|
gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
|
||||||
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw=
|
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw=
|
||||||
|
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
|
||||||
|
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
|||||||
@@ -1,205 +1,406 @@
|
|||||||
// author: AlexKraak (https://github.com/alexkraak/)
|
// author: AlexKraak (https://github.com/alexkraak/)
|
||||||
|
// author: sentriz (https://github.com/sentriz/)
|
||||||
|
|
||||||
package jukebox
|
package jukebox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/faiface/beep"
|
"github.com/dexterlb/mpvipc"
|
||||||
"github.com/faiface/beep/flac"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/faiface/beep/mp3"
|
"golang.org/x/exp/slices"
|
||||||
"github.com/faiface/beep/speaker"
|
|
||||||
|
|
||||||
"go.senan.xyz/gonic/db"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Status struct {
|
var (
|
||||||
CurrentIndex int
|
ErrMPVTimeout = fmt.Errorf("mpv not responding")
|
||||||
Playing bool
|
ErrMPVNeverStarted = fmt.Errorf("mpv never started")
|
||||||
Gain float64
|
)
|
||||||
Position int
|
|
||||||
|
func MPVArg(k string, v any) string {
|
||||||
|
if v, ok := v.(bool); ok {
|
||||||
|
if v {
|
||||||
|
return fmt.Sprintf("%s=yes", k)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s=no", k)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s=%v", k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Jukebox struct {
|
type Jukebox struct {
|
||||||
playlist []*db.Track
|
cmd *exec.Cmd
|
||||||
index int
|
conn *mpvipc.Connection
|
||||||
playing bool
|
events <-chan *mpvipc.Event
|
||||||
sr beep.SampleRate
|
|
||||||
// used to notify the player to re read the members
|
|
||||||
quit chan struct{}
|
|
||||||
done chan bool
|
|
||||||
info *strmInfo
|
|
||||||
speaker chan updateSpeaker
|
|
||||||
sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
type strmInfo struct {
|
mu sync.Mutex
|
||||||
ctrlStrmr beep.Ctrl
|
|
||||||
strm beep.StreamSeekCloser
|
|
||||||
format beep.Format
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateSpeaker struct {
|
|
||||||
index int
|
|
||||||
offset int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Jukebox {
|
func New() *Jukebox {
|
||||||
return &Jukebox{
|
return &Jukebox{}
|
||||||
sr: beep.SampleRate(48000),
|
|
||||||
speaker: make(chan updateSpeaker, 1),
|
|
||||||
done: make(chan bool),
|
|
||||||
quit: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Jukebox) Listen() error {
|
func (j *Jukebox) Start(sockPath string, mpvExtraArgs []string) error {
|
||||||
if err := speaker.Init(j.sr, j.sr.N(time.Second/2)); err != nil {
|
const mpvName = "mpv"
|
||||||
return fmt.Errorf("initing speaker: %w", err)
|
if _, err := exec.LookPath(mpvName); err != nil {
|
||||||
|
return fmt.Errorf("look path: %w. did you forget to install it?", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mpvArgs []string
|
||||||
|
mpvArgs = append(mpvArgs, "--idle", "--no-config", "--no-video", MPVArg("--audio-display", "no"), MPVArg("--input-ipc-server", sockPath))
|
||||||
|
mpvArgs = append(mpvArgs, mpvExtraArgs...)
|
||||||
|
|
||||||
|
j.cmd = exec.Command(mpvName, mpvArgs...)
|
||||||
|
if err := j.cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start mpv process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := waitUntil(5*time.Second, func() bool {
|
||||||
|
_, err := os.Stat(sockPath)
|
||||||
|
return err == nil
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
_ = j.cmd.Process.Kill()
|
||||||
|
return ErrMPVNeverStarted
|
||||||
|
}
|
||||||
|
|
||||||
|
j.conn = mpvipc.NewConnection(sockPath)
|
||||||
|
if err := j.conn.Open(); err != nil {
|
||||||
|
return fmt.Errorf("open connection: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := j.conn.Call("observe_property", 0, "seekable"); err != nil {
|
||||||
|
return fmt.Errorf("observe property: %w", err)
|
||||||
|
}
|
||||||
|
j.events, _ = j.conn.NewEventListener()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) Wait() error {
|
||||||
|
var exitError *exec.ExitError
|
||||||
|
if err := j.cmd.Wait(); err != nil && !errors.As(err, &exitError) {
|
||||||
|
return fmt.Errorf("wait jukebox: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) GetPlaylist() ([]string, error) {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
var playlist mpvPlaylist
|
||||||
|
if err := j.getDecode(&playlist, "playlist"); err != nil {
|
||||||
|
return nil, fmt.Errorf("get playlist: %w", err)
|
||||||
|
}
|
||||||
|
var items []string
|
||||||
|
for _, item := range playlist {
|
||||||
|
items = append(items, item.Filename)
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) SetPlaylist(items []string) error {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
var playlist mpvPlaylist
|
||||||
|
if err := j.getDecode(&playlist, "playlist"); err != nil {
|
||||||
|
return fmt.Errorf("get playlist: %w", err)
|
||||||
|
}
|
||||||
|
current, currentIndex := find(playlist, func(item mpvPlaylistItem) bool {
|
||||||
|
return item.Current
|
||||||
|
})
|
||||||
|
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
currFilename, _ := filepath.Rel(cwd, current.Filename)
|
||||||
|
filteredItems, foundExistingTrack := filter(items, func(filename string) bool {
|
||||||
|
return filename != currFilename
|
||||||
|
})
|
||||||
|
|
||||||
|
tmp, cleanup, err := tmp()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
for _, item := range filteredItems {
|
||||||
|
item, _ = filepath.Abs(item)
|
||||||
|
fmt.Fprintln(tmp, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundExistingTrack {
|
||||||
|
// easy case - a brand new set of tracks that we can overwrite
|
||||||
|
if _, err := j.conn.Call("loadlist", tmp.Name(), "replace"); err != nil {
|
||||||
|
return fmt.Errorf("load list: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// not so easy, we need to clear the playlist except what's playing, load everything
|
||||||
|
// except for what we're playing, then move what's playing back to its original index
|
||||||
|
// clear all items except what's playing (will be at index 0)
|
||||||
|
if _, err := j.conn.Call("playlist-clear"); err != nil {
|
||||||
|
return fmt.Errorf("clear playlist: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := j.conn.Call("loadlist", tmp.Name(), "append-play"); err != nil {
|
||||||
|
return fmt.Errorf("load list: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := j.conn.Call("playlist-move", 0, currentIndex+1); err != nil {
|
||||||
|
return fmt.Errorf("playlist move: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) AppendToPlaylist(items []string) error {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
tmp, cleanup, err := tmp()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
for _, item := range items {
|
||||||
|
fmt.Fprintln(tmp, item)
|
||||||
|
}
|
||||||
|
if _, err := j.conn.Call("loadlist", tmp.Name(), "append"); err != nil {
|
||||||
|
return fmt.Errorf("load list: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) RemovePlaylistIndex(i int) error {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
if _, err := j.conn.Call("playlist-remove", i); err != nil {
|
||||||
|
return fmt.Errorf("playlist remove: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) SkipToPlaylistIndex(i int, offsetSecs int) error {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
matchEventSeekable := func(e *mpvipc.Event) bool {
|
||||||
|
seekable, _ := e.Data.(bool)
|
||||||
|
return e.Name == "property-change" &&
|
||||||
|
e.ExtraData["name"] == "seekable" &&
|
||||||
|
seekable
|
||||||
|
}
|
||||||
|
|
||||||
|
if offsetSecs > 0 {
|
||||||
|
if err := j.conn.Set("pause", true); err != nil {
|
||||||
|
return fmt.Errorf("pause: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := j.conn.Call("playlist-play-index", i); err != nil {
|
||||||
|
return fmt.Errorf("playlist play index: %w", err)
|
||||||
|
}
|
||||||
|
if offsetSecs > 0 {
|
||||||
|
if err := waitFor(j.events, matchEventSeekable); err != nil {
|
||||||
|
return fmt.Errorf("waiting for file load: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := j.conn.Call("seek", offsetSecs, "absolute"); err != nil {
|
||||||
|
return fmt.Errorf("seek: %w", err)
|
||||||
|
}
|
||||||
|
if err := j.conn.Set("pause", false); err != nil {
|
||||||
|
return fmt.Errorf("play: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) ClearPlaylist() error {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
if _, err := j.conn.Call("playlist-clear"); err != nil {
|
||||||
|
return fmt.Errorf("seek: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) Pause() error {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
if err := j.conn.Set("pause", true); err != nil {
|
||||||
|
return fmt.Errorf("pause: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) Play() error {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
if err := j.conn.Set("pause", false); err != nil {
|
||||||
|
return fmt.Errorf("pause: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) SetVolumePct(v int) error {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
if err := j.conn.Set("volume", v); err != nil {
|
||||||
|
return fmt.Errorf("set volume: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) GetVolumePct() (float64, error) {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
var volume float64
|
||||||
|
if err := j.getDecode(&volume, "volume"); err != nil {
|
||||||
|
return 0, fmt.Errorf("get volume: %w", err)
|
||||||
|
}
|
||||||
|
return volume, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
CurrentIndex int
|
||||||
|
CurrentFilename string
|
||||||
|
Length int
|
||||||
|
Playing bool
|
||||||
|
GainPct int
|
||||||
|
Position int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) GetStatus() (*Status, error) {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
var status Status
|
||||||
|
_ = j.getDecode(&status.Position, "time-pos") // property may not always be there
|
||||||
|
_ = j.getDecode(&status.GainPct, "volume") // property may not always be there
|
||||||
|
|
||||||
|
var playlist mpvPlaylist
|
||||||
|
_ = j.getDecode(&playlist, "playlist")
|
||||||
|
|
||||||
|
status.CurrentIndex = slices.IndexFunc(playlist, func(pl mpvPlaylistItem) bool {
|
||||||
|
return pl.Current
|
||||||
|
})
|
||||||
|
|
||||||
|
status.Length = len(playlist)
|
||||||
|
|
||||||
|
if status.CurrentIndex < 0 {
|
||||||
|
return &status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
status.CurrentFilename = playlist[status.CurrentIndex].Filename
|
||||||
|
|
||||||
|
var paused bool
|
||||||
|
_ = j.getDecode(&paused, "pause") // property may not always be there
|
||||||
|
status.Playing = !paused
|
||||||
|
|
||||||
|
return &status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) Quit() error {
|
||||||
|
defer lock(&j.mu)()
|
||||||
|
|
||||||
|
if j.conn == nil || j.conn.IsClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := j.conn.Call("quit"); err != nil {
|
||||||
|
return fmt.Errorf("quit: %w", err)
|
||||||
|
}
|
||||||
|
if err := j.conn.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close: %w", err)
|
||||||
|
}
|
||||||
|
j.conn.WaitUntilClosed()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jukebox) getDecode(dest any, property string) error {
|
||||||
|
raw, err := j.conn.Get(property)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get property: %w", err)
|
||||||
|
}
|
||||||
|
if err := mapstructure.Decode(raw, dest); err != nil {
|
||||||
|
return fmt.Errorf("decode: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mpvPlaylist []mpvPlaylistItem
|
||||||
|
type mpvPlaylistItem struct {
|
||||||
|
ID int
|
||||||
|
Filename string
|
||||||
|
Current bool
|
||||||
|
Playing bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitUntil(timeout time.Duration, f func() bool) bool {
|
||||||
|
quit := time.NewTicker(timeout)
|
||||||
|
defer quit.Stop()
|
||||||
|
check := time.NewTicker(100 * time.Millisecond)
|
||||||
|
defer check.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-j.quit:
|
case <-quit.C:
|
||||||
return nil
|
return false
|
||||||
case speaker := <-j.speaker:
|
case <-check.C:
|
||||||
if err := j.doUpdateSpeaker(speaker); err != nil {
|
if f() {
|
||||||
log.Printf("error in jukebox: %v", err)
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Jukebox) Quit() {
|
func waitFor[T any](ch <-chan T, match func(e T) bool) error {
|
||||||
j.quit <- struct{}{}
|
quit := time.NewTicker(1 * time.Second)
|
||||||
}
|
defer quit.Stop()
|
||||||
|
|
||||||
func (j *Jukebox) doUpdateSpeaker(su updateSpeaker) error {
|
defer time.Sleep(350 * time.Millisecond)
|
||||||
j.Lock()
|
|
||||||
defer j.Unlock()
|
for {
|
||||||
if su.index >= len(j.playlist) {
|
select {
|
||||||
j.playing = false
|
case <-quit.C:
|
||||||
speaker.Clear()
|
return ErrMPVTimeout
|
||||||
|
case ev := <-ch:
|
||||||
|
if match(ev) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
j.index = su.index
|
}
|
||||||
f, err := os.Open(j.playlist[su.index].AbsPath())
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tmp() (*os.File, func(), error) {
|
||||||
|
tmp, err := os.CreateTemp("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, fmt.Errorf("create temp file: %w", err)
|
||||||
}
|
}
|
||||||
var streamer beep.Streamer
|
cleanup := func() {
|
||||||
var format beep.Format
|
os.Remove(tmp.Name())
|
||||||
switch j.playlist[su.index].Ext() {
|
tmp.Close()
|
||||||
case "mp3":
|
|
||||||
streamer, format, err = mp3.Decode(f)
|
|
||||||
case "flac":
|
|
||||||
streamer, format, err = flac.Decode(f)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
return tmp, cleanup, nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
j.info = &strmInfo{}
|
|
||||||
j.info.strm = streamer.(beep.StreamSeekCloser)
|
|
||||||
if su.offset != 0 {
|
|
||||||
samples := format.SampleRate.N(time.Second * time.Duration(su.offset))
|
|
||||||
if err := j.info.strm.Seek(samples); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
j.info.ctrlStrmr.Streamer = beep.Resample(
|
|
||||||
4, format.SampleRate,
|
|
||||||
j.sr, j.info.strm,
|
|
||||||
)
|
|
||||||
j.info.format = format
|
|
||||||
speaker.Play(beep.Seq(&j.info.ctrlStrmr, beep.Callback(func() {
|
|
||||||
j.speaker <- updateSpeaker{index: su.index + 1}
|
|
||||||
})))
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Jukebox) SetTracks(tracks []*db.Track) {
|
func find[T any](items []T, f func(T) bool) (T, int) {
|
||||||
j.Lock()
|
for i, item := range items {
|
||||||
defer j.Unlock()
|
if f(item) {
|
||||||
j.playlist = tracks
|
return item, i
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Jukebox) AddTracks(tracks []*db.Track) {
|
|
||||||
j.Lock()
|
|
||||||
if len(j.playlist) == 0 {
|
|
||||||
j.playlist = tracks
|
|
||||||
j.playing = true
|
|
||||||
j.index = 0
|
|
||||||
j.Unlock()
|
|
||||||
j.speaker <- updateSpeaker{index: 0}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
j.playlist = append(j.playlist, tracks...)
|
|
||||||
j.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Jukebox) RemoveTrack(i int) {
|
|
||||||
j.Lock()
|
|
||||||
defer j.Unlock()
|
|
||||||
if i < 0 || i >= len(j.playlist) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
j.playlist = append(j.playlist[:i], j.playlist[i+1:]...)
|
var t T
|
||||||
|
return t, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Jukebox) Skip(i int, offset int) {
|
func filter[T comparable](items []T, f func(T) bool) ([]T, bool) {
|
||||||
speaker.Clear()
|
var found bool
|
||||||
j.Lock()
|
var ret []T
|
||||||
j.index = i
|
for _, item := range items {
|
||||||
j.playing = true
|
if !f(item) {
|
||||||
j.Unlock()
|
found = true
|
||||||
j.speaker <- updateSpeaker{index: j.index, offset: offset}
|
continue
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Jukebox) ClearTracks() {
|
|
||||||
speaker.Clear()
|
|
||||||
j.Lock()
|
|
||||||
defer j.Unlock()
|
|
||||||
j.playing = false
|
|
||||||
j.playlist = []*db.Track{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Jukebox) Stop() {
|
|
||||||
j.Lock()
|
|
||||||
defer j.Unlock()
|
|
||||||
if j.info != nil {
|
|
||||||
j.playing = false
|
|
||||||
j.info.ctrlStrmr.Paused = true
|
|
||||||
}
|
}
|
||||||
|
ret = append(ret, item)
|
||||||
|
}
|
||||||
|
return ret, found
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Jukebox) Start() {
|
func lock(mu *sync.Mutex) func() {
|
||||||
if j.info != nil {
|
mu.Lock()
|
||||||
j.playing = true
|
return mu.Unlock
|
||||||
j.info.ctrlStrmr.Paused = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Jukebox) GetStatus() Status {
|
|
||||||
j.Lock()
|
|
||||||
defer j.Unlock()
|
|
||||||
position := 0
|
|
||||||
if j.info != nil {
|
|
||||||
length := j.info.format.SampleRate.D(j.info.strm.Position())
|
|
||||||
position = int(length.Round(time.Millisecond).Seconds())
|
|
||||||
}
|
|
||||||
return Status{
|
|
||||||
CurrentIndex: j.index,
|
|
||||||
Playing: j.playing,
|
|
||||||
Gain: 0.9,
|
|
||||||
Position: position,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Jukebox) GetTracks() []*db.Track {
|
|
||||||
j.Lock()
|
|
||||||
defer j.Unlock()
|
|
||||||
return j.playlist
|
|
||||||
}
|
}
|
||||||
|
|||||||
187
jukebox/jukebox_test.go
Normal file
187
jukebox/jukebox_test.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package jukebox_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"go.senan.xyz/gonic/jukebox"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newJukebox(t *testing.T) *jukebox.Jukebox {
|
||||||
|
sockPath := filepath.Join(t.TempDir(), "mpv.sock")
|
||||||
|
|
||||||
|
j := jukebox.New()
|
||||||
|
err := j.Start(
|
||||||
|
sockPath,
|
||||||
|
[]string{jukebox.MPVArg("--ao", "null")},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("start jukebox: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
j.Quit()
|
||||||
|
})
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlaySkipReset(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
j := newJukebox(t)
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
is.NoErr(j.SetPlaylist([]string{
|
||||||
|
testPath("tr_0.mp3"),
|
||||||
|
testPath("tr_1.mp3"),
|
||||||
|
testPath("tr_2.mp3"),
|
||||||
|
testPath("tr_3.mp3"),
|
||||||
|
testPath("tr_4.mp3"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
status, err := j.GetStatus()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(status.CurrentIndex, 0)
|
||||||
|
is.Equal(status.CurrentFilename, testPath("tr_0.mp3"))
|
||||||
|
is.Equal(status.Length, 5)
|
||||||
|
is.Equal(status.Playing, true)
|
||||||
|
|
||||||
|
items, err := j.GetPlaylist()
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
itemsSorted := append([]string(nil), items...)
|
||||||
|
sort.Strings(itemsSorted)
|
||||||
|
is.Equal(items, itemsSorted)
|
||||||
|
|
||||||
|
is.NoErr(j.Play())
|
||||||
|
|
||||||
|
status, err = j.GetStatus()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(status.Playing, true)
|
||||||
|
|
||||||
|
is.NoErr(j.Pause())
|
||||||
|
|
||||||
|
status, err = j.GetStatus()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(status.Playing, false)
|
||||||
|
|
||||||
|
is.NoErr(j.Play())
|
||||||
|
|
||||||
|
// skip to 2
|
||||||
|
is.NoErr(j.SkipToPlaylistIndex(2, 0))
|
||||||
|
|
||||||
|
status, err = j.GetStatus()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(status.CurrentIndex, 2)
|
||||||
|
is.Equal(status.CurrentFilename, testPath("tr_2.mp3"))
|
||||||
|
is.Equal(status.Length, 5)
|
||||||
|
is.Equal(status.Playing, true)
|
||||||
|
|
||||||
|
// skip to 3
|
||||||
|
is.NoErr(j.SkipToPlaylistIndex(3, 0))
|
||||||
|
|
||||||
|
status, err = j.GetStatus()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(status.CurrentIndex, 3)
|
||||||
|
is.Equal(status.CurrentFilename, testPath("tr_3.mp3"))
|
||||||
|
is.Equal(status.Length, 5)
|
||||||
|
is.Equal(status.Playing, true)
|
||||||
|
|
||||||
|
// just add one more by overwriting the playlist like some clients do
|
||||||
|
// we should keep the current track unchaned if we find it
|
||||||
|
is.NoErr(j.SetPlaylist([]string{
|
||||||
|
"testdata/tr_0.mp3",
|
||||||
|
"testdata/tr_1.mp3",
|
||||||
|
"testdata/tr_2.mp3",
|
||||||
|
"testdata/tr_3.mp3",
|
||||||
|
"testdata/tr_4.mp3",
|
||||||
|
"testdata/tr_5.mp3",
|
||||||
|
}))
|
||||||
|
|
||||||
|
status, err = j.GetStatus()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(status.CurrentIndex, 3) // index unchanged
|
||||||
|
is.Equal(status.CurrentFilename, testPath("tr_3.mp3"))
|
||||||
|
is.Equal(status.Length, 6) // we added one more track
|
||||||
|
is.Equal(status.Playing, true)
|
||||||
|
|
||||||
|
// skip to 3 again
|
||||||
|
is.NoErr(j.SkipToPlaylistIndex(3, 0))
|
||||||
|
|
||||||
|
status, err = j.GetStatus()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(status.CurrentIndex, 3)
|
||||||
|
is.Equal(status.CurrentFilename, testPath("tr_3.mp3"))
|
||||||
|
is.Equal(status.Length, 6)
|
||||||
|
is.Equal(status.Playing, true)
|
||||||
|
|
||||||
|
// remove all but 3
|
||||||
|
is.NoErr(j.SetPlaylist([]string{
|
||||||
|
"testdata/tr_0.mp3",
|
||||||
|
"testdata/tr_1.mp3",
|
||||||
|
"testdata/tr_2.mp3",
|
||||||
|
"testdata/tr_3.mp3",
|
||||||
|
}))
|
||||||
|
|
||||||
|
status, err = j.GetStatus()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(status.CurrentIndex, 3) // index unchanged
|
||||||
|
is.Equal(status.CurrentFilename, testPath("tr_3.mp3"))
|
||||||
|
is.Equal(status.Length, 4)
|
||||||
|
is.Equal(status.Playing, true)
|
||||||
|
|
||||||
|
// skip to 2 (5s long) in the middle of the track
|
||||||
|
is.NoErr(j.SkipToPlaylistIndex(2, 2))
|
||||||
|
|
||||||
|
status, err = j.GetStatus()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(status.CurrentIndex, 2) // index unchanged
|
||||||
|
is.Equal(status.CurrentFilename, testPath("tr_2.mp3"))
|
||||||
|
is.Equal(status.Length, 4)
|
||||||
|
is.Equal(status.Playing, true)
|
||||||
|
is.Equal(status.Position, 2) // at new position
|
||||||
|
|
||||||
|
// overwrite completely
|
||||||
|
is.NoErr(j.SetPlaylist([]string{
|
||||||
|
"testdata/tr_5.mp3",
|
||||||
|
"testdata/tr_6.mp3",
|
||||||
|
"testdata/tr_7.mp3",
|
||||||
|
"testdata/tr_8.mp3",
|
||||||
|
"testdata/tr_9.mp3",
|
||||||
|
}))
|
||||||
|
|
||||||
|
status, err = j.GetStatus()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(status.CurrentIndex, 0) // index unchanged
|
||||||
|
is.Equal(status.CurrentFilename, testPath("tr_5.mp3"))
|
||||||
|
is.Equal(status.Length, 5)
|
||||||
|
is.Equal(status.Playing, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVolume(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
j := newJukebox(t)
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
vol, err := j.GetVolumePct()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(vol, 100.0)
|
||||||
|
|
||||||
|
is.NoErr(j.SetVolumePct(69.0))
|
||||||
|
|
||||||
|
vol, err = j.GetVolumePct()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(vol, 69.0)
|
||||||
|
|
||||||
|
is.NoErr(j.SetVolumePct(0.0))
|
||||||
|
|
||||||
|
vol, err = j.GetVolumePct()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(vol, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPath(path string) string {
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
return filepath.Join(cwd, "testdata", path)
|
||||||
|
}
|
||||||
BIN
jukebox/testdata/10s.mp3
vendored
Normal file
BIN
jukebox/testdata/10s.mp3
vendored
Normal file
Binary file not shown.
BIN
jukebox/testdata/5s.mp3
vendored
Normal file
BIN
jukebox/testdata/5s.mp3
vendored
Normal file
Binary file not shown.
1
jukebox/testdata/tr_0.mp3
vendored
Symbolic link
1
jukebox/testdata/tr_0.mp3
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
5s.mp3
|
||||||
1
jukebox/testdata/tr_1.mp3
vendored
Symbolic link
1
jukebox/testdata/tr_1.mp3
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
5s.mp3
|
||||||
1
jukebox/testdata/tr_2.mp3
vendored
Symbolic link
1
jukebox/testdata/tr_2.mp3
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
5s.mp3
|
||||||
1
jukebox/testdata/tr_3.mp3
vendored
Symbolic link
1
jukebox/testdata/tr_3.mp3
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
5s.mp3
|
||||||
1
jukebox/testdata/tr_4.mp3
vendored
Symbolic link
1
jukebox/testdata/tr_4.mp3
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
5s.mp3
|
||||||
1
jukebox/testdata/tr_5.mp3
vendored
Symbolic link
1
jukebox/testdata/tr_5.mp3
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
5s.mp3
|
||||||
1
jukebox/testdata/tr_6.mp3
vendored
Symbolic link
1
jukebox/testdata/tr_6.mp3
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
5s.mp3
|
||||||
1
jukebox/testdata/tr_7.mp3
vendored
Symbolic link
1
jukebox/testdata/tr_7.mp3
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
5s.mp3
|
||||||
1
jukebox/testdata/tr_8.mp3
vendored
Symbolic link
1
jukebox/testdata/tr_8.mp3
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
5s.mp3
|
||||||
1
jukebox/testdata/tr_9.mp3
vendored
Symbolic link
1
jukebox/testdata/tr_9.mp3
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
5s.mp3
|
||||||
@@ -2,8 +2,11 @@ package ctrlsubsonic
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
@@ -270,80 +273,135 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
|
|||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeJukebox(r *http.Request) *spec.Response {
|
func (c *Controller) ServeJukebox(r *http.Request) *spec.Response { // nolint:gocyclo
|
||||||
params := r.Context().Value(CtxParams).(params.Params)
|
params := r.Context().Value(CtxParams).(params.Params)
|
||||||
user := r.Context().Value(CtxUser).(*db.User)
|
user := r.Context().Value(CtxUser).(*db.User)
|
||||||
getTracks := func() []*db.Track {
|
trackPaths := func(ids []specid.ID) ([]string, error) {
|
||||||
var tracks []*db.Track
|
var paths []string
|
||||||
ids, err := params.GetIDList("id")
|
|
||||||
if err != nil {
|
|
||||||
return tracks
|
|
||||||
}
|
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
track := &db.Track{}
|
var track db.Track
|
||||||
c.DB.
|
if err := c.DB.Preload("Album").Preload("TrackStar", "user_id=?", user.ID).Preload("TrackRating", "user_id=?", user.ID).First(&track, id.Value).Error; err != nil {
|
||||||
Preload("Album").
|
return nil, fmt.Errorf("find track by id: %w", err)
|
||||||
Preload("TrackStar", "user_id=?", user.ID).
|
|
||||||
Preload("TrackRating", "user_id=?", user.ID).
|
|
||||||
First(track, id.Value)
|
|
||||||
if track.ID != 0 {
|
|
||||||
tracks = append(tracks, track)
|
|
||||||
}
|
}
|
||||||
|
paths = append(paths, track.AbsPath())
|
||||||
}
|
}
|
||||||
return tracks
|
return paths, nil
|
||||||
}
|
}
|
||||||
getStatus := func() spec.JukeboxStatus {
|
getSpecStatus := func() (*spec.JukeboxStatus, error) {
|
||||||
status := c.Jukebox.GetStatus()
|
status, err := c.Jukebox.GetStatus()
|
||||||
return spec.JukeboxStatus{
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get status: %w", err)
|
||||||
|
}
|
||||||
|
return &spec.JukeboxStatus{
|
||||||
CurrentIndex: status.CurrentIndex,
|
CurrentIndex: status.CurrentIndex,
|
||||||
Playing: status.Playing,
|
Playing: status.Playing,
|
||||||
Gain: status.Gain,
|
Gain: float64(status.GainPct) / 100.0,
|
||||||
Position: status.Position,
|
Position: status.Position,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
getSpecPlaylist := func() ([]*spec.TrackChild, error) {
|
||||||
|
var ret []*spec.TrackChild
|
||||||
|
playlist, err := c.Jukebox.GetPlaylist()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get playlist: %w", err)
|
||||||
}
|
}
|
||||||
getStatusTracks := func() []*spec.TrackChild {
|
for _, path := range playlist {
|
||||||
tracks := c.Jukebox.GetTracks()
|
cwd, _ := os.Getwd()
|
||||||
ret := make([]*spec.TrackChild, len(tracks))
|
path, _ = filepath.Rel(cwd, path)
|
||||||
for i, track := range tracks {
|
var track db.Track
|
||||||
ret[i] = spec.NewTrackByTags(track, track.Album)
|
err := c.DB.
|
||||||
|
Preload("Album").
|
||||||
|
Where(`(albums.root_dir || ? || albums.left_path || albums.right_path || ? || tracks.filename)=?`,
|
||||||
|
string(filepath.Separator), string(filepath.Separator), path).
|
||||||
|
Joins(`JOIN albums ON tracks.album_id=albums.id`).
|
||||||
|
First(&track).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetch track: %w", err)
|
||||||
}
|
}
|
||||||
return ret
|
ret = append(ret, spec.NewTrackByTags(&track, track.Album))
|
||||||
}
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
switch act, _ := params.Get("action"); act {
|
switch act, _ := params.Get("action"); act {
|
||||||
case "set":
|
case "set":
|
||||||
c.Jukebox.SetTracks(getTracks())
|
ids := params.GetOrIDList("id", nil)
|
||||||
|
paths, err := trackPaths(ids)
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(0, "error creating playlist items: %v", err)
|
||||||
|
}
|
||||||
|
if err := c.Jukebox.SetPlaylist(paths); err != nil {
|
||||||
|
return spec.NewError(0, "error setting playlist: %v", err)
|
||||||
|
}
|
||||||
case "add":
|
case "add":
|
||||||
c.Jukebox.AddTracks(getTracks())
|
ids := params.GetOrIDList("id", nil)
|
||||||
|
paths, err := trackPaths(ids)
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(10, "error creating playlist items: %v", err)
|
||||||
|
}
|
||||||
|
if err := c.Jukebox.AppendToPlaylist(paths); err != nil {
|
||||||
|
return spec.NewError(0, "error appending to playlist: %v", err)
|
||||||
|
}
|
||||||
case "clear":
|
case "clear":
|
||||||
c.Jukebox.ClearTracks()
|
if err := c.Jukebox.ClearPlaylist(); err != nil {
|
||||||
|
return spec.NewError(0, "error clearing playlist: %v", err)
|
||||||
|
}
|
||||||
case "remove":
|
case "remove":
|
||||||
index, err := params.GetInt("index")
|
index, err := params.GetInt("index")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return spec.NewError(10, "please provide an id for remove actions")
|
return spec.NewError(10, "please provide an id for remove actions")
|
||||||
}
|
}
|
||||||
c.Jukebox.RemoveTrack(index)
|
if err := c.Jukebox.RemovePlaylistIndex(index); err != nil {
|
||||||
|
return spec.NewError(0, "error removing: %v", err)
|
||||||
|
}
|
||||||
case "stop":
|
case "stop":
|
||||||
c.Jukebox.Stop()
|
if err := c.Jukebox.Pause(); err != nil {
|
||||||
|
return spec.NewError(0, "error stopping: %v", err)
|
||||||
|
}
|
||||||
case "start":
|
case "start":
|
||||||
c.Jukebox.Start()
|
if err := c.Jukebox.Play(); err != nil {
|
||||||
|
return spec.NewError(0, "error starting: %v", err)
|
||||||
|
}
|
||||||
case "skip":
|
case "skip":
|
||||||
index, err := params.GetInt("index")
|
index, err := params.GetInt("index")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return spec.NewError(10, "please provide an index for skip actions")
|
return spec.NewError(10, "please provide an index for skip actions")
|
||||||
}
|
}
|
||||||
offset, _ := params.GetInt("offset")
|
offset, _ := params.GetInt("offset")
|
||||||
c.Jukebox.Skip(index, offset)
|
if err := c.Jukebox.SkipToPlaylistIndex(index, offset); err != nil {
|
||||||
|
return spec.NewError(0, "error skipping: %v", err)
|
||||||
|
}
|
||||||
case "get":
|
case "get":
|
||||||
|
specPlaylist, err := getSpecPlaylist()
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(10, "error getting status tracks: %v", err)
|
||||||
|
}
|
||||||
|
status, err := getSpecStatus()
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(10, "error getting status: %v", err)
|
||||||
|
}
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.JukeboxPlaylist = &spec.JukeboxPlaylist{
|
sub.JukeboxPlaylist = &spec.JukeboxPlaylist{
|
||||||
JukeboxStatus: getStatus(),
|
JukeboxStatus: status,
|
||||||
List: getStatusTracks(),
|
List: specPlaylist,
|
||||||
}
|
}
|
||||||
return sub
|
return sub
|
||||||
|
case "setGain":
|
||||||
|
gain, err := params.GetFloat("gain")
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(10, "please provide a valid gain param")
|
||||||
|
}
|
||||||
|
if err := c.Jukebox.SetVolumePct(int(math.Min(gain, 1) * 100)); err != nil {
|
||||||
|
return spec.NewError(0, "error setting gain: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// all actions except get are expected to return a status
|
// all actions except get are expected to return a status
|
||||||
|
status, err := getSpecStatus()
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(10, "error getting status: %v", err)
|
||||||
|
}
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
status := getStatus()
|
sub.JukeboxStatus = status
|
||||||
sub.JukeboxStatus = &status
|
|
||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ type JukeboxStatus struct {
|
|||||||
|
|
||||||
type JukeboxPlaylist struct {
|
type JukeboxPlaylist struct {
|
||||||
List []*TrackChild `xml:"entry,omitempty" json:"entry,omitempty"`
|
List []*TrackChild `xml:"entry,omitempty" json:"entry,omitempty"`
|
||||||
JukeboxStatus
|
*JukeboxStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
type Podcasts struct {
|
type Podcasts struct {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -100,7 +101,6 @@ func New(opts Options) (*Server, error) {
|
|||||||
CoverCachePath: opts.CoverCachePath,
|
CoverCachePath: opts.CoverCachePath,
|
||||||
PodcastsPath: opts.PodcastPath,
|
PodcastsPath: opts.PodcastPath,
|
||||||
MusicPaths: opts.MusicPaths,
|
MusicPaths: opts.MusicPaths,
|
||||||
Jukebox: &jukebox.Jukebox{},
|
|
||||||
Scrobblers: []scrobble.Scrobbler{&lastfm.Scrobbler{DB: opts.DB}, &listenbrainz.Scrobbler{}},
|
Scrobblers: []scrobble.Scrobbler{&lastfm.Scrobbler{DB: opts.DB}, &listenbrainz.Scrobbler{}},
|
||||||
Podcasts: podcast,
|
Podcasts: podcast,
|
||||||
Transcoder: cacheTranscoder,
|
Transcoder: cacheTranscoder,
|
||||||
@@ -360,13 +360,29 @@ func (s *Server) StartScanWatcher() (FuncExecute, FuncInterrupt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) StartJukebox() (FuncExecute, FuncInterrupt) {
|
func (s *Server) StartJukebox(mpvExtraArgs []string) (FuncExecute, FuncInterrupt) {
|
||||||
|
var sockFile *os.File
|
||||||
return func() error {
|
return func() error {
|
||||||
log.Printf("starting job 'jukebox'\n")
|
log.Printf("starting job 'jukebox'\n")
|
||||||
return s.jukebox.Listen()
|
var err error
|
||||||
|
sockFile, err = os.CreateTemp("", "gonic-jukebox-*.sock")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create tmp sock file: %w", err)
|
||||||
|
}
|
||||||
|
if err := s.jukebox.Start(sockFile.Name(), mpvExtraArgs); err != nil {
|
||||||
|
return fmt.Errorf("start jukebox: %w", err)
|
||||||
|
}
|
||||||
|
if err := s.jukebox.Wait(); err != nil {
|
||||||
|
return fmt.Errorf("start jukebox: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}, func(_ error) {
|
}, func(_ error) {
|
||||||
// stop job
|
// stop job
|
||||||
s.jukebox.Quit()
|
if err := s.jukebox.Quit(); err != nil {
|
||||||
|
log.Printf("error quitting jukebox: %v", err)
|
||||||
|
}
|
||||||
|
_ = sockFile.Close()
|
||||||
|
_ = os.Remove(sockFile.Name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user