feat(jukebox): use mpv over ipc as a player backend

This commit is contained in:
sentriz
2022-11-16 18:28:31 +00:00
committed by Senan Kelly
parent ec97289d45
commit e1488b0d18
26 changed files with 695 additions and 269 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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 \

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
return nil case ev := <-ch:
} if match(ev) {
j.index = su.index return nil
f, err := os.Open(j.playlist[su.index].AbsPath()) }
if err != nil {
return err
}
var streamer beep.Streamer
var format beep.Format
switch j.playlist[su.index].Ext() {
case "mp3":
streamer, format, err = mp3.Decode(f)
case "flac":
streamer, format, err = flac.Decode(f)
}
if err != 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 tmp() (*os.File, func(), error) {
j.Lock() tmp, err := os.CreateTemp("", "")
defer j.Unlock() if err != nil {
j.playlist = tracks return nil, nil, fmt.Errorf("create temp file: %w", err)
}
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...) cleanup := func() {
j.Unlock() os.Remove(tmp.Name())
} tmp.Close()
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:]...) return tmp, cleanup, nil
} }
func (j *Jukebox) Skip(i int, offset int) { func find[T any](items []T, f func(T) bool) (T, int) {
speaker.Clear() for i, item := range items {
j.Lock() if f(item) {
j.index = i return item, i
j.playing = true }
j.Unlock()
j.speaker <- updateSpeaker{index: j.index, offset: offset}
}
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
} }
var t T
return t, -1
} }
func (j *Jukebox) Start() { func filter[T comparable](items []T, f func(T) bool) ([]T, bool) {
if j.info != nil { var found bool
j.playing = true var ret []T
j.info.ctrlStrmr.Paused = false for _, item := range items {
if !f(item) {
found = true
continue
}
ret = append(ret, item)
} }
return ret, found
} }
func (j *Jukebox) GetStatus() Status { func lock(mu *sync.Mutex) func() {
j.Lock() mu.Lock()
defer j.Unlock() return mu.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
View 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

Binary file not shown.

BIN
jukebox/testdata/5s.mp3 vendored Normal file

Binary file not shown.

1
jukebox/testdata/tr_0.mp3 vendored Symbolic link
View File

@@ -0,0 +1 @@
5s.mp3

1
jukebox/testdata/tr_1.mp3 vendored Symbolic link
View File

@@ -0,0 +1 @@
5s.mp3

1
jukebox/testdata/tr_2.mp3 vendored Symbolic link
View File

@@ -0,0 +1 @@
5s.mp3

1
jukebox/testdata/tr_3.mp3 vendored Symbolic link
View File

@@ -0,0 +1 @@
5s.mp3

1
jukebox/testdata/tr_4.mp3 vendored Symbolic link
View File

@@ -0,0 +1 @@
5s.mp3

1
jukebox/testdata/tr_5.mp3 vendored Symbolic link
View File

@@ -0,0 +1 @@
5s.mp3

1
jukebox/testdata/tr_6.mp3 vendored Symbolic link
View File

@@ -0,0 +1 @@
5s.mp3

1
jukebox/testdata/tr_7.mp3 vendored Symbolic link
View File

@@ -0,0 +1 @@
5s.mp3

1
jukebox/testdata/tr_8.mp3 vendored Symbolic link
View File

@@ -0,0 +1 @@
5s.mp3

1
jukebox/testdata/tr_9.mp3 vendored Symbolic link
View File

@@ -0,0 +1 @@
5s.mp3

View File

@@ -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
} }
getStatusTracks := func() []*spec.TrackChild { getSpecPlaylist := func() ([]*spec.TrackChild, error) {
tracks := c.Jukebox.GetTracks() var ret []*spec.TrackChild
ret := make([]*spec.TrackChild, len(tracks)) playlist, err := c.Jukebox.GetPlaylist()
for i, track := range tracks { if err != nil {
ret[i] = spec.NewTrackByTags(track, track.Album) return nil, fmt.Errorf("get playlist: %w", err)
} }
return ret for _, path := range playlist {
cwd, _ := os.Getwd()
path, _ = filepath.Rel(cwd, path)
var track db.Track
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)
}
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
} }

View File

@@ -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 {

View File

@@ -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())
} }
} }