refactor: update scanner, scanner tests, mockfs
closes #165 closes #163
This commit is contained in:
4
.github/workflows/nightly-release.yaml
vendored
4
.github/workflows/nightly-release.yaml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: Nightly Release
|
name: Nightly Release
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
- cron: "0 0 * * *"
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
version: v1.40.0
|
version: v1.42.1
|
||||||
skip-go-installation: true
|
skip-go-installation: true
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|||||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
version: v1.40.0
|
version: v1.42.1
|
||||||
skip-go-installation: true
|
skip-go-installation: true
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|||||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- name: Lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
version: v1.40.0
|
version: v1.42.1
|
||||||
skip-go-installation: true
|
skip-go-installation: true
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|||||||
@@ -1,57 +1,51 @@
|
|||||||
run:
|
run:
|
||||||
skip-dirs:
|
skip-dirs:
|
||||||
- server/assets
|
- server/assets
|
||||||
skip-dirs-use-default: true
|
skip-dirs-use-default: true
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- deadcode
|
- deadcode
|
||||||
- depguard
|
- depguard
|
||||||
- dogsled
|
- dogsled
|
||||||
- errcheck
|
- errcheck
|
||||||
- exportloopref
|
- exportloopref
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- goerr113
|
- goerr113
|
||||||
- golint
|
- goprintffuncname
|
||||||
- goprintffuncname
|
- gosec
|
||||||
- gosec
|
- gosimple
|
||||||
- gosimple
|
- govet
|
||||||
- govet
|
- ineffassign
|
||||||
- ineffassign
|
- misspell
|
||||||
- lll
|
- nakedret
|
||||||
- misspell
|
- revive
|
||||||
- nakedret
|
- rowserrcheck
|
||||||
- rowserrcheck
|
- staticcheck
|
||||||
- staticcheck
|
- structcheck
|
||||||
- structcheck
|
- stylecheck
|
||||||
- stylecheck
|
- typecheck
|
||||||
- typecheck
|
- unconvert
|
||||||
- unconvert
|
- varcheck
|
||||||
- varcheck
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
- path: _test\.go
|
- path: _test\.go
|
||||||
linters:
|
linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
- text: "weak cryptographic primitive"
|
- text: "weak cryptographic primitive"
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
- text: "weak random number generator"
|
- text: "weak random number generator"
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
|
- text: "at least one file in a package should have a package comment"
|
||||||
# TODO: fix these
|
linters:
|
||||||
- text: "should have comment"
|
- stylecheck
|
||||||
linters:
|
|
||||||
- golint
|
|
||||||
- text: "at least one file in a package should have a package comment"
|
|
||||||
linters:
|
|
||||||
- stylecheck
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := db.New(*confDBPath)
|
dbc, err := db.New(*confDBPath, db.DefaultOptions())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error opening database: %v\n", err)
|
log.Fatalf("error opening database: %v\n", err)
|
||||||
}
|
}
|
||||||
@@ -106,8 +106,7 @@ func main() {
|
|||||||
JukeboxEnabled: *confJukeboxEnabled,
|
JukeboxEnabled: *confJukeboxEnabled,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error creating server: %v\n", err)
|
log.Panicf("error creating server: %v\n", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var g run.Group
|
var g run.Group
|
||||||
@@ -123,6 +122,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := g.Run(); err != nil {
|
if err := g.Run(); err != nil {
|
||||||
log.Printf("error in job: %v", err)
|
log.Panicf("error in job: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
test_listen_addr=localhost:9353
|
|
||||||
test_data_path=server/ctrlsubsonic/testdata
|
|
||||||
test_db_path=$test_data_path/db
|
|
||||||
|
|
||||||
test_music_path=~/music
|
|
||||||
test_podcast_path="$(mktemp -d)"
|
|
||||||
test_cache_path="$(mktemp -d)"
|
|
||||||
|
|
||||||
mkdir "$test_music_path" 2>/dev/null
|
|
||||||
echo "waiting for server to start"
|
|
||||||
go run cmd/gonic/gonic.go \
|
|
||||||
-music-path "$test_music_path" \
|
|
||||||
-podcast-path "$test_podcast_path" \
|
|
||||||
-cache-path "$test_cache_path" \
|
|
||||||
-db-path "$test_db_path" \
|
|
||||||
-listen-addr "$test_listen_addr" 2>&1 \
|
|
||||||
| while read line; do
|
|
||||||
echo "from server: $line"
|
|
||||||
[[ "$line" != *$'starting job \'http\''* ]] && continue
|
|
||||||
sleep '0.5'
|
|
||||||
# ** begin by folder
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbumList.view?c=c&f=json&p=admin&u=admin&v=v&type=alphabeticalByArtist" | jq >"$test_data_path/test_get_album_list_alpha_artist"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbumList.view?c=c&f=json&p=admin&u=admin&v=v&type=alphabeticalByName" | jq >"$test_data_path/test_get_album_list_alpha_name"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbumList.view?c=c&f=json&p=admin&u=admin&v=v&type=alphabeticalByName" | jq >"$test_data_path/test_get_album_list_two_alpha_name"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbumList.view?c=c&f=json&p=admin&u=admin&v=v&type=newest" | jq >"$test_data_path/test_get_album_list_newest"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbumList.view?c=c&f=json&p=admin&u=admin&v=v&type=random&size=15" | jq >"$test_data_path/test_get_album_list_random"
|
|
||||||
curl -s "http://$test_listen_addr/rest/search2.view?c=c&f=json&p=admin&u=admin&v=v&query=13" | jq >"$test_data_path/test_search_two_q_13"
|
|
||||||
curl -s "http://$test_listen_addr/rest/search2.view?c=c&f=json&p=admin&u=admin&v=v&query=ani" | jq >"$test_data_path/test_search_two_q_ani"
|
|
||||||
curl -s "http://$test_listen_addr/rest/search2.view?c=c&f=json&p=admin&u=admin&v=v&query=cert" | jq >"$test_data_path/test_search_two_q_cert"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getIndexes.view?c=c&p=admin&u=admin&v=v&f=json" | jq >"$test_data_path/test_get_indexes_no_args"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getMusicDirectory.view?c=Jamsstash&id=al-2&p=admin&u=admin&v=v&f=json" | jq >"$test_data_path/test_get_music_directory_without_tracks"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getMusicDirectory.view?c=Jamsstash&id=al-3&p=admin&u=admin&v=v&f=json" | jq >"$test_data_path/test_get_music_directory_with_tracks"
|
|
||||||
# ** begin by tags
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbum.view?c=c&f=json&p=admin&u=admin&v=v&id=al-2" | jq >"$test_data_path/test_get_album_without_cover"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbum.view?c=c&f=json&p=admin&u=admin&v=v&id=al-3" | jq >"$test_data_path/test_get_album_with_cover"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbumList2.view?c=c&f=json&p=admin&u=admin&v=v&type=alphabeticalByArtist" | jq >"$test_data_path/test_get_album_list_two_alpha_artist"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbumList2.view?c=c&f=json&p=admin&u=admin&v=v&type=alphabeticalByName" | jq >"$test_data_path/test_get_album_list_two_alpha_name"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbumList2.view?c=c&f=json&p=admin&u=admin&v=v&type=newest" | jq >"$test_data_path/test_get_album_list_two_newest"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getAlbumList2.view?c=c&f=json&p=admin&u=admin&v=v&type=random&size=15" | jq >"$test_data_path/test_get_album_list_two_random"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getArtist.view?c=c&f=json&p=admin&u=admin&v=v&id=ar-1" | jq >"$test_data_path/test_get_artist_id_one"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getArtist.view?c=c&f=json&p=admin&u=admin&v=v&id=ar-2" | jq >"$test_data_path/test_get_artist_id_two"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getArtist.view?c=c&f=json&p=admin&u=admin&v=v&id=ar-3" | jq >"$test_data_path/test_get_artist_id_three"
|
|
||||||
curl -s "http://$test_listen_addr/rest/getArtists.view?c=c&f=json&p=admin&u=admin&v=v" | jq >"$test_data_path/test_get_artists_no_args"
|
|
||||||
curl -s "http://$test_listen_addr/rest/search3.view?c=c&f=json&p=admin&u=admin&v=v&query=13" | jq >"$test_data_path/test_search_three_q_13"
|
|
||||||
curl -s "http://$test_listen_addr/rest/search3.view?c=c&f=json&p=admin&u=admin&v=v&query=ani" | jq >"$test_data_path/test_search_three_q_ani"
|
|
||||||
curl -s "http://$test_listen_addr/rest/search3.view?c=c&f=json&p=admin&u=admin&v=v&query=cert" | jq >"$test_data_path/test_search_three_q_cert"
|
|
||||||
#
|
|
||||||
pkill -INT -f "/tmp/go-build.*$test_listen_addr.*"
|
|
||||||
done
|
|
||||||
29
go.mod
29
go.mod
@@ -9,9 +9,8 @@ require (
|
|||||||
github.com/cespare/xxhash v1.1.0
|
github.com/cespare/xxhash v1.1.0
|
||||||
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.0.3-0.20210817042730-1c98bf641535
|
github.com/faiface/beep v1.1.0
|
||||||
github.com/google/uuid v1.1.2 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gopherjs/gopherwasm v1.0.0 // indirect
|
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/gorilla/securecookie v1.1.1
|
github.com/gorilla/securecookie v1.1.1
|
||||||
github.com/gorilla/sessions v1.2.1
|
github.com/gorilla/sessions v1.2.1
|
||||||
@@ -21,23 +20,21 @@ require (
|
|||||||
github.com/jinzhu/gorm v1.9.16
|
github.com/jinzhu/gorm v1.9.16
|
||||||
github.com/josephburnett/jd v0.0.0-20191228205456-aa1a7c66b42f
|
github.com/josephburnett/jd v0.0.0-20191228205456-aa1a7c66b42f
|
||||||
github.com/karrick/godirwalk v1.16.1
|
github.com/karrick/godirwalk v1.16.1
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/matryer/is v1.4.0
|
||||||
github.com/mewkiz/pkg v0.0.0-20200702171441-dd47075182ea // indirect
|
github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77 // indirect
|
||||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.1 // indirect
|
github.com/mmcdole/gofeed v1.1.3
|
||||||
github.com/mmcdole/gofeed v1.1.0
|
github.com/mmcdole/goxpp v0.0.0-20200921145534-2f3784f67354 // indirect
|
||||||
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd
|
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd
|
||||||
github.com/oklog/run v1.1.0
|
github.com/oklog/run v1.1.0
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
||||||
github.com/peterbourgon/ff v1.7.0
|
github.com/peterbourgon/ff v1.7.0
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
||||||
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23
|
github.com/wader/gormstore v0.0.0-20211009162750-8bf4f5606ef4
|
||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
|
golang.org/x/exp v0.0.0-20211103171733-83d51122435b // indirect
|
||||||
golang.org/x/exp v0.0.0-20201229011636-eab1b5eb1a03 // indirect
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
||||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
|
golang.org/x/mobile v0.0.0-20211103151657-e68c98865fb2 // indirect
|
||||||
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 // indirect
|
golang.org/x/net v0.0.0-20211104170005-ce137452f963 // indirect
|
||||||
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect
|
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
|
||||||
gopkg.in/gormigrate.v1 v1.6.0
|
gopkg.in/gormigrate.v1 v1.6.0
|
||||||
)
|
)
|
||||||
|
|||||||
79
go.sum
79
go.sum
@@ -31,12 +31,13 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4
|
|||||||
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.0.2 h1:UB5DiRNmA4erfUYnHbgU4UB6DlBOrsdEFRtcc8sCkdQ=
|
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
|
||||||
github.com/faiface/beep v1.0.2/go.mod h1:1yLb5yRdHMsovYYWVqYLioXkVuziCSITW1oarTeduQM=
|
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
||||||
github.com/faiface/beep v1.0.3-0.20210817042730-1c98bf641535 h1:391d1LXITcjNUsoeXUY21E5UCsmFz/W3ft9sInjynDI=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/faiface/beep v1.0.3-0.20210817042730-1c98bf641535/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||||
|
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
github.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ=
|
|
||||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
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/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/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||||
@@ -53,10 +54,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
|
||||||
github.com/gopherjs/gopherwasm v0.1.1/go.mod h1:kx4n9a+MzHH0BJJhvlsQ65hqLFXDO/m256AsaDPQ+/4=
|
|
||||||
github.com/gopherjs/gopherwasm v1.0.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
|
|
||||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
@@ -66,15 +63,10 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
|
|||||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
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.1.1/go.mod h1:4i+c5pDNKDrxl1iu9iG90/+fhP37lio6gNhjCx9WBJw=
|
|
||||||
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||||
github.com/hajimehoshi/go-mp3 v0.3.1 h1:pn/SKU1+/rfK8KaZXdGEC2G/KCB2aLRjbTCrwKcokao=
|
github.com/hajimehoshi/go-mp3 v0.3.1 h1:pn/SKU1+/rfK8KaZXdGEC2G/KCB2aLRjbTCrwKcokao=
|
||||||
github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||||
github.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQdfpwz04=
|
|
||||||
github.com/hajimehoshi/oto v0.3.1/go.mod h1:e9eTLBB9iZto045HLbzfHJIc+jP3xaKrjZTghvb6fdM=
|
|
||||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||||
github.com/hajimehoshi/oto v0.7.0 h1:4HbTRhNuHd4SdFfA4vhIgmwvVO3qWueHK+fF1cButpg=
|
|
||||||
github.com/hajimehoshi/oto v0.7.0/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
|
||||||
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
|
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
|
||||||
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
||||||
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
||||||
@@ -83,9 +75,10 @@ github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8=
|
|||||||
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
github.com/icza/bitio v1.0.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 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
|
||||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
||||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||||
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
|
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/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=
|
||||||
@@ -98,6 +91,9 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
|||||||
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
|
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
|
||||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
|
||||||
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
github.com/josephburnett/jd v0.0.0-20191228205456-aa1a7c66b42f h1:ijUonnyvDekPD7lUF4oQ1LV+dKaTnchEzmenMFa6NL4=
|
github.com/josephburnett/jd v0.0.0-20191228205456-aa1a7c66b42f h1:ijUonnyvDekPD7lUF4oQ1LV+dKaTnchEzmenMFa6NL4=
|
||||||
@@ -106,26 +102,24 @@ github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr
|
|||||||
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/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
|
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
|
||||||
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
|
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|
||||||
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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
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 v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||||
|
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||||
|
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
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 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs=
|
|
||||||
github.com/mewkiz/flac v1.0.6 h1:OnMwCWZPAnjDndjEzLynOZ71Y2U+/QYHoVI4JEKgKkk=
|
|
||||||
github.com/mewkiz/flac v1.0.6/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
|
|
||||||
github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
|
github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
|
||||||
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
|
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-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
|
||||||
@@ -134,16 +128,19 @@ github.com/mewkiz/pkg v0.0.0-20200702171441-dd47075182ea/go.mod h1:3E2FUC/qYUfM8
|
|||||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
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/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||||
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
|
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||||
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mmcdole/gofeed v1.1.0 h1:T2WrGLVJRV04PY2qwhEJLHCt9JiCtBhb6SmC8ZvJH08=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mmcdole/gofeed v1.1.0/go.mod h1:PPiVwgDXLlz2N83KB4TrIim2lyYM5Zn7ZWH9Pi4oHUk=
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
|
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/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE=
|
||||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
|
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd h1:xKn/gU8lZupoZt/HE7a/R3aH93iUO6JwyRsYelQUsRI=
|
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd h1:xKn/gU8lZupoZt/HE7a/R3aH93iUO6JwyRsYelQUsRI=
|
||||||
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd/go.mod h1:B6icauz2l4tkYQxmDtCH4qmNWz/evSW5CsOqp6IE5IE=
|
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd/go.mod h1:B6icauz2l4tkYQxmDtCH4qmNWz/evSW5CsOqp6IE5IE=
|
||||||
@@ -179,19 +176,16 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||||
golang.org/x/exp v0.0.0-20201229011636-eab1b5eb1a03 h1:XlAInxBYX5nBofPaY51uv/x9xmRgZGr/lDOsePd2AcE=
|
golang.org/x/exp v0.0.0-20201229011636-eab1b5eb1a03 h1:XlAInxBYX5nBofPaY51uv/x9xmRgZGr/lDOsePd2AcE=
|
||||||
golang.org/x/exp v0.0.0-20201229011636-eab1b5eb1a03/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4=
|
golang.org/x/exp v0.0.0-20201229011636-eab1b5eb1a03/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4=
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
|
||||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
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-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
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.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
|
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
|
||||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/mobile v0.0.0-20180806140643-507816974b79/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f h1:kgfVkAEEQXXQ0qc6dH7n6y37NAYmTFmz0YRwrRjgxKw=
|
golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f h1:kgfVkAEEQXXQ0qc6dH7n6y37NAYmTFmz0YRwrRjgxKw=
|
||||||
@@ -202,6 +196,14 @@ golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hM
|
|||||||
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
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=
|
||||||
@@ -211,10 +213,10 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
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/sys v0.0.0-20181228144115-9a3f9b0469bb/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-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-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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/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-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -236,10 +238,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbO
|
|||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import "strings"
|
|||||||
type Err []error
|
type Err []error
|
||||||
|
|
||||||
func (me Err) Error() string {
|
func (me Err) Error() string {
|
||||||
var strs []string
|
var builder strings.Builder
|
||||||
for _, err := range me {
|
for _, err := range me {
|
||||||
strs = append(strs, err.Error())
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString(err.Error())
|
||||||
}
|
}
|
||||||
return strings.Join(strs, "\n")
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (me Err) Len() int {
|
func (me Err) Len() int {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func firstExisting(or string, strings ...string) string {
|
|||||||
|
|
||||||
func doScan(scanner *scanner.Scanner, opts scanner.ScanOptions) {
|
func doScan(scanner *scanner.Scanner, opts scanner.ScanOptions) {
|
||||||
go func() {
|
go func() {
|
||||||
if err := scanner.Start(opts); err != nil {
|
if err := scanner.ScanAndClean(opts); err != nil {
|
||||||
log.Printf("error while scanning: %v\n", err)
|
log.Printf("error while scanning: %v\n", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -43,11 +43,11 @@ func (c *Controller) ServeLogin(r *http.Request) *Response {
|
|||||||
|
|
||||||
func (c *Controller) ServeHome(r *http.Request) *Response {
|
func (c *Controller) ServeHome(r *http.Request) *Response {
|
||||||
data := &templateData{}
|
data := &templateData{}
|
||||||
// ** begin stats box
|
// stats box
|
||||||
c.DB.Table("artists").Count(&data.ArtistCount)
|
c.DB.Model(&db.Artist{}).Count(&data.ArtistCount)
|
||||||
c.DB.Table("albums").Count(&data.AlbumCount)
|
c.DB.Model(&db.Album{}).Count(&data.AlbumCount)
|
||||||
c.DB.Table("tracks").Count(&data.TrackCount)
|
c.DB.Table("tracks").Count(&data.TrackCount)
|
||||||
// ** begin lastfm box
|
// lastfm box
|
||||||
scheme := firstExisting(
|
scheme := firstExisting(
|
||||||
"http", // fallback
|
"http", // fallback
|
||||||
r.Header.Get("X-Forwarded-Proto"),
|
r.Header.Get("X-Forwarded-Proto"),
|
||||||
@@ -60,36 +60,37 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
|
|||||||
r.Host,
|
r.Host,
|
||||||
)
|
)
|
||||||
data.RequestRoot = fmt.Sprintf("%s://%s", scheme, host)
|
data.RequestRoot = fmt.Sprintf("%s://%s", scheme, host)
|
||||||
data.CurrentLastFMAPIKey = c.DB.GetSetting("lastfm_api_key")
|
data.CurrentLastFMAPIKey, _ = c.DB.GetSetting("lastfm_api_key")
|
||||||
data.DefaultListenBrainzURL = listenbrainz.BaseURL
|
data.DefaultListenBrainzURL = listenbrainz.BaseURL
|
||||||
// ** begin users box
|
// users box
|
||||||
c.DB.Find(&data.AllUsers)
|
c.DB.Find(&data.AllUsers)
|
||||||
// ** begin recent folders box
|
// recent folders box
|
||||||
c.DB.
|
c.DB.
|
||||||
Where("tag_artist_id IS NOT NULL").
|
Where("tag_artist_id IS NOT NULL").
|
||||||
Order("modified_at DESC").
|
Order("modified_at DESC").
|
||||||
Limit(8).
|
Limit(8).
|
||||||
Find(&data.RecentFolders)
|
Find(&data.RecentFolders)
|
||||||
data.IsScanning = scanner.IsScanning()
|
data.IsScanning = c.Scanner.IsScanning()
|
||||||
if tStr := c.DB.GetSetting("last_scan_time"); tStr != "" {
|
if tStr, err := c.DB.GetSetting("last_scan_time"); err != nil {
|
||||||
i, _ := strconv.ParseInt(tStr, 10, 64)
|
i, _ := strconv.ParseInt(tStr, 10, 64)
|
||||||
data.LastScanTime = time.Unix(i, 0)
|
data.LastScanTime = time.Unix(i, 0)
|
||||||
}
|
}
|
||||||
//
|
|
||||||
user := r.Context().Value(CtxUser).(*db.User)
|
user := r.Context().Value(CtxUser).(*db.User)
|
||||||
// ** begin playlists box
|
|
||||||
|
// playlists box
|
||||||
c.DB.
|
c.DB.
|
||||||
Where("user_id=?", user.ID).
|
Where("user_id=?", user.ID).
|
||||||
Limit(20).
|
Limit(20).
|
||||||
Find(&data.Playlists)
|
Find(&data.Playlists)
|
||||||
// ** begin transcoding box
|
// transcoding box
|
||||||
c.DB.
|
c.DB.
|
||||||
Where("user_id=?", user.ID).
|
Where("user_id=?", user.ID).
|
||||||
Find(&data.TranscodePreferences)
|
Find(&data.TranscodePreferences)
|
||||||
for profile := range encode.Profiles() {
|
for profile := range encode.Profiles() {
|
||||||
data.TranscodeProfiles = append(data.TranscodeProfiles, profile)
|
data.TranscodeProfiles = append(data.TranscodeProfiles, profile)
|
||||||
}
|
}
|
||||||
// ** begin podcasts box
|
// podcasts box
|
||||||
c.DB.Find(&data.Podcasts)
|
c.DB.Find(&data.Podcasts)
|
||||||
//
|
//
|
||||||
return &Response{
|
return &Response{
|
||||||
@@ -143,11 +144,15 @@ func (c *Controller) ServeLinkLastFMDo(r *http.Request) *Response {
|
|||||||
code: 400,
|
code: 400,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sessionKey, err := lastfm.GetSession(
|
apiKey, err := c.DB.GetSetting("lastfm_api_key")
|
||||||
c.DB.GetSetting("lastfm_api_key"),
|
if err != nil {
|
||||||
c.DB.GetSetting("lastfm_secret"),
|
return &Response{code: 500, err: fmt.Sprintf("couldn't get api key: %v", err)}
|
||||||
token,
|
}
|
||||||
)
|
secret, err := c.DB.GetSetting("lastfm_secret")
|
||||||
|
if err != nil {
|
||||||
|
return &Response{code: 500, err: fmt.Sprintf("couldn't get secret: %v", err)}
|
||||||
|
}
|
||||||
|
sessionKey, err := lastfm.GetSession(apiKey, secret, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &Response{
|
return &Response{
|
||||||
redirect: "/admin/home",
|
redirect: "/admin/home",
|
||||||
@@ -341,8 +346,13 @@ func (c *Controller) ServeCreateUserDo(r *http.Request) *Response {
|
|||||||
|
|
||||||
func (c *Controller) ServeUpdateLastFMAPIKey(r *http.Request) *Response {
|
func (c *Controller) ServeUpdateLastFMAPIKey(r *http.Request) *Response {
|
||||||
data := &templateData{}
|
data := &templateData{}
|
||||||
data.CurrentLastFMAPIKey = c.DB.GetSetting("lastfm_api_key")
|
var err error
|
||||||
data.CurrentLastFMAPISecret = c.DB.GetSetting("lastfm_secret")
|
if data.CurrentLastFMAPIKey, err = c.DB.GetSetting("lastfm_api_key"); err != nil {
|
||||||
|
return &Response{code: 500, err: fmt.Sprintf("couldn't get api key: %v", err)}
|
||||||
|
}
|
||||||
|
if data.CurrentLastFMAPISecret, err = c.DB.GetSetting("lastfm_secret"); err != nil {
|
||||||
|
return &Response{code: 500, err: fmt.Sprintf("couldn't get secret: %v", err)}
|
||||||
|
}
|
||||||
return &Response{
|
return &Response{
|
||||||
template: "update_lastfm_api_key.tmpl",
|
template: "update_lastfm_api_key.tmpl",
|
||||||
data: data,
|
data: data,
|
||||||
@@ -358,8 +368,12 @@ func (c *Controller) ServeUpdateLastFMAPIKeyDo(r *http.Request) *Response {
|
|||||||
flashW: []string{err.Error()},
|
flashW: []string{err.Error()},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.DB.SetSetting("lastfm_api_key", apiKey)
|
if err := c.DB.SetSetting("lastfm_api_key", apiKey); err != nil {
|
||||||
c.DB.SetSetting("lastfm_secret", secret)
|
return &Response{code: 500, err: fmt.Sprintf("couldn't set api key: %v", err)}
|
||||||
|
}
|
||||||
|
if err := c.DB.SetSetting("lastfm_secret", secret); err != nil {
|
||||||
|
return &Response{code: 500, err: fmt.Sprintf("couldn't set secret: %v", err)}
|
||||||
|
}
|
||||||
return &Response{redirect: "/admin/home"}
|
return &Response{redirect: "/admin/home"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func playlistParseLine(c *Controller, path string) (int, error) {
|
|||||||
c.MusicPath, path)
|
c.MusicPath, path)
|
||||||
err := query.First(&track).Error
|
err := query.First(&track).Error
|
||||||
switch {
|
switch {
|
||||||
case gorm.IsRecordNotFoundError(err):
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
return 0, fmt.Errorf("%v: %w", err, errPlaylistNoMatch)
|
return 0, fmt.Errorf("%v: %w", err, errPlaylistNoMatch)
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return 0, fmt.Errorf("while matching: %w", err)
|
return 0, fmt.Errorf("while matching: %w", err)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Controller struct {
|
|||||||
*ctrlbase.Controller
|
*ctrlbase.Controller
|
||||||
CachePath string
|
CachePath string
|
||||||
CoverCachePath string
|
CoverCachePath string
|
||||||
|
PodcastsPath string
|
||||||
Jukebox *jukebox.Jukebox
|
Jukebox *jukebox.Jukebox
|
||||||
Scrobblers []scrobble.Scrobbler
|
Scrobblers []scrobble.Scrobbler
|
||||||
Podcasts *podcasts.Podcasts
|
Podcasts *podcasts.Podcasts
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ctrlsubsonic
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -16,14 +17,12 @@ import (
|
|||||||
|
|
||||||
"go.senan.xyz/gonic/server/ctrlbase"
|
"go.senan.xyz/gonic/server/ctrlbase"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
||||||
"go.senan.xyz/gonic/server/db"
|
"go.senan.xyz/gonic/server/mockfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testDataDir = "testdata"
|
testDataDir = "testdata"
|
||||||
testCamelExpr = regexp.MustCompile("([a-z0-9])([A-Z])")
|
testCamelExpr = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||||
testDBPath = path.Join(testDataDir, "db")
|
|
||||||
testController *Controller
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type queryCase struct {
|
type queryCase struct {
|
||||||
@@ -53,18 +52,26 @@ func makeHTTPMock(query url.Values) (*httptest.ResponseRecorder, *http.Request)
|
|||||||
return rr, req
|
return rr, req
|
||||||
}
|
}
|
||||||
|
|
||||||
func runQueryCases(t *testing.T, h handlerSubsonic, cases []*queryCase) {
|
func runQueryCases(t *testing.T, contr *Controller, h handlerSubsonic, cases []*queryCase) {
|
||||||
|
t.Helper()
|
||||||
for _, qc := range cases {
|
for _, qc := range cases {
|
||||||
qc := qc // pin
|
qc := qc // pin
|
||||||
t.Run(qc.expectPath, func(t *testing.T) {
|
t.Run(qc.expectPath, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
rr, req := makeHTTPMock(qc.params)
|
rr, req := makeHTTPMock(qc.params)
|
||||||
testController.H(h).ServeHTTP(rr, req)
|
contr.H(h).ServeHTTP(rr, req)
|
||||||
body := rr.Body.String()
|
body := rr.Body.String()
|
||||||
if status := rr.Code; status != http.StatusOK {
|
if status := rr.Code; status != http.StatusOK {
|
||||||
t.Fatalf("didn't give a 200\n%s", body)
|
t.Fatalf("didn't give a 200\n%s", body)
|
||||||
}
|
}
|
||||||
goldenPath := makeGoldenPath(t.Name())
|
goldenPath := makeGoldenPath(t.Name())
|
||||||
|
goldenRegen := os.Getenv("GONIC_REGEN")
|
||||||
|
if goldenRegen == "*" || (goldenRegen != "" && strings.HasPrefix(t.Name(), goldenRegen)) {
|
||||||
|
_ = os.WriteFile(goldenPath, []byte(body), 0600)
|
||||||
|
t.Logf("golden file %q regenerated for %s", goldenPath, t.Name())
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
// read case to differ with handler result
|
// read case to differ with handler result
|
||||||
expected, err := jd.ReadJsonFile(goldenPath)
|
expected, err := jd.ReadJsonFile(goldenPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -88,13 +95,26 @@ func runQueryCases(t *testing.T, h handlerSubsonic, cases []*queryCase) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeController(t *testing.T) (*Controller, *mockfs.MockFS) { return makec(t, []string{""}) }
|
||||||
|
func makeControllerRoots(t *testing.T, r []string) (*Controller, *mockfs.MockFS) { return makec(t, r) }
|
||||||
|
|
||||||
|
func makec(t *testing.T, roots []string) (*Controller, *mockfs.MockFS) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
m := mockfs.NewWithDirs(t, roots)
|
||||||
|
for _, root := range roots {
|
||||||
|
m.AddItemsPrefixWithCovers(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.ScanAndClean()
|
||||||
|
m.ResetDates()
|
||||||
|
m.LogAlbums()
|
||||||
|
|
||||||
|
base := &ctrlbase.Controller{DB: m.DB()}
|
||||||
|
return &Controller{Controller: base}, m
|
||||||
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
db, err := db.New(testDBPath)
|
log.SetOutput(ioutil.Discard)
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error opening database: %v\n", err)
|
|
||||||
}
|
|
||||||
testController = &Controller{
|
|
||||||
Controller: &ctrlbase.Controller{DB: db},
|
|
||||||
}
|
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ctrlsubsonic
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
@@ -18,7 +19,7 @@ func (c *Controller) ServeGetBookmarks(r *http.Request) *spec.Response {
|
|||||||
Where("user_id=?", user.ID).
|
Where("user_id=?", user.ID).
|
||||||
Find(&bookmarks).
|
Find(&bookmarks).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return spec.NewResponse()
|
return spec.NewResponse()
|
||||||
}
|
}
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
|
|||||||
childrenObj := []*spec.TrackChild{}
|
childrenObj := []*spec.TrackChild{}
|
||||||
folder := &db.Album{}
|
folder := &db.Album{}
|
||||||
c.DB.First(folder, id.Value)
|
c.DB.First(folder, id.Value)
|
||||||
// ** begin start looking for child childFolders in the current dir
|
// start looking for child childFolders in the current dir
|
||||||
var childFolders []*db.Album
|
var childFolders []*db.Album
|
||||||
c.DB.
|
c.DB.
|
||||||
Where("parent_id=?", id.Value).
|
Where("parent_id=?", id.Value).
|
||||||
@@ -70,7 +70,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
|
|||||||
for _, c := range childFolders {
|
for _, c := range childFolders {
|
||||||
childrenObj = append(childrenObj, spec.NewTCAlbumByFolder(c))
|
childrenObj = append(childrenObj, spec.NewTCAlbumByFolder(c))
|
||||||
}
|
}
|
||||||
// ** begin start looking for child childTracks in the current dir
|
// start looking for child childTracks in the current dir
|
||||||
var childTracks []*db.Track
|
var childTracks []*db.Track
|
||||||
c.DB.
|
c.DB.
|
||||||
Where("album_id=?", id.Value).
|
Where("album_id=?", id.Value).
|
||||||
@@ -86,7 +86,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
|
|||||||
}
|
}
|
||||||
childrenObj = append(childrenObj, toAppend)
|
childrenObj = append(childrenObj, toAppend)
|
||||||
}
|
}
|
||||||
// ** begin respond section
|
// respond section
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.Directory = spec.NewDirectoryByFolder(folder, childrenObj)
|
sub.Directory = spec.NewDirectoryByFolder(folder, childrenObj)
|
||||||
return sub
|
return sub
|
||||||
@@ -167,7 +167,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
|
|||||||
}
|
}
|
||||||
query = fmt.Sprintf("%%%s%%", strings.TrimSuffix(query, "*"))
|
query = fmt.Sprintf("%%%s%%", strings.TrimSuffix(query, "*"))
|
||||||
results := &spec.SearchResultTwo{}
|
results := &spec.SearchResultTwo{}
|
||||||
// ** begin search "artists"
|
// search "artists"
|
||||||
var artists []*db.Album
|
var artists []*db.Album
|
||||||
c.DB.
|
c.DB.
|
||||||
Where(`
|
Where(`
|
||||||
@@ -182,7 +182,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
|
|||||||
results.Artists = append(results.Artists,
|
results.Artists = append(results.Artists,
|
||||||
spec.NewDirectoryByFolder(a, nil))
|
spec.NewDirectoryByFolder(a, nil))
|
||||||
}
|
}
|
||||||
// ** begin search "albums"
|
// search "albums"
|
||||||
var albums []*db.Album
|
var albums []*db.Album
|
||||||
c.DB.
|
c.DB.
|
||||||
Where(`
|
Where(`
|
||||||
@@ -196,7 +196,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
|
|||||||
for _, a := range albums {
|
for _, a := range albums {
|
||||||
results.Albums = append(results.Albums, spec.NewTCAlbumByFolder(a))
|
results.Albums = append(results.Albums, spec.NewTCAlbumByFolder(a))
|
||||||
}
|
}
|
||||||
// ** begin search tracks
|
// search tracks
|
||||||
var tracks []*db.Track
|
var tracks []*db.Track
|
||||||
c.DB.
|
c.DB.
|
||||||
Preload("Album").
|
Preload("Album").
|
||||||
|
|||||||
@@ -8,20 +8,30 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetIndexes(t *testing.T) {
|
func TestGetIndexes(t *testing.T) {
|
||||||
runQueryCases(t, testController.ServeGetIndexes, []*queryCase{
|
contr, m := makeControllerRoots(t, []string{"m-0", "m-1"})
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
runQueryCases(t, contr, contr.ServeGetIndexes, []*queryCase{
|
||||||
{url.Values{}, "no_args", false},
|
{url.Values{}, "no_args", false},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetMusicDirectory(t *testing.T) {
|
func TestGetMusicDirectory(t *testing.T) {
|
||||||
runQueryCases(t, testController.ServeGetMusicDirectory, []*queryCase{
|
contr, m := makeController(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
runQueryCases(t, contr, contr.ServeGetMusicDirectory, []*queryCase{
|
||||||
{url.Values{"id": {"al-2"}}, "without_tracks", false},
|
{url.Values{"id": {"al-2"}}, "without_tracks", false},
|
||||||
{url.Values{"id": {"al-3"}}, "with_tracks", false},
|
{url.Values{"id": {"al-3"}}, "with_tracks", false},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAlbumList(t *testing.T) {
|
func TestGetAlbumList(t *testing.T) {
|
||||||
runQueryCases(t, testController.ServeGetAlbumList, []*queryCase{
|
t.Parallel()
|
||||||
|
contr, m := makeController(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
runQueryCases(t, contr, contr.ServeGetAlbumList, []*queryCase{
|
||||||
{url.Values{"type": {"alphabeticalByArtist"}}, "alpha_artist", false},
|
{url.Values{"type": {"alphabeticalByArtist"}}, "alpha_artist", false},
|
||||||
{url.Values{"type": {"alphabeticalByName"}}, "alpha_name", false},
|
{url.Values{"type": {"alphabeticalByName"}}, "alpha_name", false},
|
||||||
{url.Values{"type": {"newest"}}, "newest", false},
|
{url.Values{"type": {"newest"}}, "newest", false},
|
||||||
@@ -30,9 +40,13 @@ func TestGetAlbumList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchTwo(t *testing.T) {
|
func TestSearchTwo(t *testing.T) {
|
||||||
runQueryCases(t, testController.ServeSearchTwo, []*queryCase{
|
t.Parallel()
|
||||||
{url.Values{"query": {"13"}}, "q_13", false},
|
contr, m := makeController(t)
|
||||||
{url.Values{"query": {"ani"}}, "q_ani", false},
|
defer m.CleanUp()
|
||||||
{url.Values{"query": {"cert"}}, "q_cert", false},
|
|
||||||
|
runQueryCases(t, contr, contr.ServeSearchTwo, []*queryCase{
|
||||||
|
{url.Values{"query": {"art"}}, "q_art", false},
|
||||||
|
{url.Values{"query": {"alb"}}, "q_alb", false},
|
||||||
|
{url.Values{"query": {"tra"}}, "q_tra", false},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ctrlsubsonic
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -88,7 +89,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
|
|||||||
}).
|
}).
|
||||||
First(album, id.Value).
|
First(album, id.Value).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return spec.NewError(10, "couldn't find an album with that id")
|
return spec.NewError(10, "couldn't find an album with that id")
|
||||||
}
|
}
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
@@ -174,7 +175,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
|
|||||||
query = fmt.Sprintf("%%%s%%",
|
query = fmt.Sprintf("%%%s%%",
|
||||||
strings.TrimSuffix(query, "*"))
|
strings.TrimSuffix(query, "*"))
|
||||||
results := &spec.SearchResultThree{}
|
results := &spec.SearchResultThree{}
|
||||||
// ** begin search "artists"
|
// search "artists"
|
||||||
var artists []*db.Artist
|
var artists []*db.Artist
|
||||||
c.DB.
|
c.DB.
|
||||||
Where("name LIKE ? OR name_u_dec LIKE ?",
|
Where("name LIKE ? OR name_u_dec LIKE ?",
|
||||||
@@ -186,7 +187,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
|
|||||||
results.Artists = append(results.Artists,
|
results.Artists = append(results.Artists,
|
||||||
spec.NewArtistByTags(a))
|
spec.NewArtistByTags(a))
|
||||||
}
|
}
|
||||||
// ** begin search "albums"
|
// search "albums"
|
||||||
var albums []*db.Album
|
var albums []*db.Album
|
||||||
c.DB.
|
c.DB.
|
||||||
Preload("TagArtist").
|
Preload("TagArtist").
|
||||||
@@ -199,7 +200,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
|
|||||||
results.Albums = append(results.Albums,
|
results.Albums = append(results.Albums,
|
||||||
spec.NewAlbumByTags(a, a.TagArtist))
|
spec.NewAlbumByTags(a, a.TagArtist))
|
||||||
}
|
}
|
||||||
// ** begin search tracks
|
// search tracks
|
||||||
var tracks []*db.Track
|
var tracks []*db.Track
|
||||||
c.DB.
|
c.DB.
|
||||||
Preload("Album").
|
Preload("Album").
|
||||||
@@ -223,7 +224,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return spec.NewError(10, "please provide an `id` parameter")
|
return spec.NewError(10, "please provide an `id` parameter")
|
||||||
}
|
}
|
||||||
apiKey := c.DB.GetSetting("lastfm_api_key")
|
apiKey, _ := c.DB.GetSetting("lastfm_api_key")
|
||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.ArtistInfoTwo = &spec.ArtistInfo{}
|
sub.ArtistInfoTwo = &spec.ArtistInfo{}
|
||||||
@@ -234,7 +235,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
|
|||||||
Where("id=?", id.Value).
|
Where("id=?", id.Value).
|
||||||
Find(artist).
|
Find(artist).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return spec.NewError(70, "artist with id `%s` not found", id)
|
return spec.NewError(70, "artist with id `%s` not found", id)
|
||||||
}
|
}
|
||||||
info, err := lastfm.ArtistGetInfo(apiKey, artist)
|
info, err := lastfm.ArtistGetInfo(apiKey, artist)
|
||||||
@@ -271,7 +272,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
|
|||||||
Group("artists.id").
|
Group("artists.id").
|
||||||
Find(artist).
|
Find(artist).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) && !inclNotPresent {
|
if errors.Is(err, gorm.ErrRecordNotFound) && !inclNotPresent {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
similar := &spec.SimilarArtist{
|
similar := &spec.SimilarArtist{
|
||||||
|
|||||||
@@ -6,13 +6,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetArtists(t *testing.T) {
|
func TestGetArtists(t *testing.T) {
|
||||||
runQueryCases(t, testController.ServeGetArtists, []*queryCase{
|
t.Parallel()
|
||||||
|
contr, m := makeController(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
runQueryCases(t, contr, contr.ServeGetArtists, []*queryCase{
|
||||||
{url.Values{}, "no_args", false},
|
{url.Values{}, "no_args", false},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetArtist(t *testing.T) {
|
func TestGetArtist(t *testing.T) {
|
||||||
runQueryCases(t, testController.ServeGetArtist, []*queryCase{
|
t.Parallel()
|
||||||
|
contr, m := makeController(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
runQueryCases(t, contr, contr.ServeGetArtist, []*queryCase{
|
||||||
{url.Values{"id": {"ar-1"}}, "id_one", false},
|
{url.Values{"id": {"ar-1"}}, "id_one", false},
|
||||||
{url.Values{"id": {"ar-2"}}, "id_two", false},
|
{url.Values{"id": {"ar-2"}}, "id_two", false},
|
||||||
{url.Values{"id": {"ar-3"}}, "id_three", false},
|
{url.Values{"id": {"ar-3"}}, "id_three", false},
|
||||||
@@ -20,14 +28,22 @@ func TestGetArtist(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAlbum(t *testing.T) {
|
func TestGetAlbum(t *testing.T) {
|
||||||
runQueryCases(t, testController.ServeGetAlbum, []*queryCase{
|
t.Parallel()
|
||||||
|
contr, m := makeController(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
runQueryCases(t, contr, contr.ServeGetAlbum, []*queryCase{
|
||||||
{url.Values{"id": {"al-2"}}, "without_cover", false},
|
{url.Values{"id": {"al-2"}}, "without_cover", false},
|
||||||
{url.Values{"id": {"al-3"}}, "with_cover", false},
|
{url.Values{"id": {"al-3"}}, "with_cover", false},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAlbumListTwo(t *testing.T) {
|
func TestGetAlbumListTwo(t *testing.T) {
|
||||||
runQueryCases(t, testController.ServeGetAlbumListTwo, []*queryCase{
|
t.Parallel()
|
||||||
|
contr, m := makeController(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
runQueryCases(t, contr, contr.ServeGetAlbumListTwo, []*queryCase{
|
||||||
{url.Values{"type": {"alphabeticalByArtist"}}, "alpha_artist", false},
|
{url.Values{"type": {"alphabeticalByArtist"}}, "alpha_artist", false},
|
||||||
{url.Values{"type": {"alphabeticalByName"}}, "alpha_name", false},
|
{url.Values{"type": {"alphabeticalByName"}}, "alpha_name", false},
|
||||||
{url.Values{"type": {"newest"}}, "newest", false},
|
{url.Values{"type": {"newest"}}, "newest", false},
|
||||||
@@ -36,9 +52,13 @@ func TestGetAlbumListTwo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchThree(t *testing.T) {
|
func TestSearchThree(t *testing.T) {
|
||||||
runQueryCases(t, testController.ServeSearchThree, []*queryCase{
|
t.Parallel()
|
||||||
{url.Values{"query": {"13"}}, "q_13", false},
|
contr, m := makeController(t)
|
||||||
{url.Values{"query": {"ani"}}, "q_ani", false},
|
defer m.CleanUp()
|
||||||
{url.Values{"query": {"cert"}}, "q_cert", false},
|
|
||||||
|
runQueryCases(t, contr, contr.ServeSearchThree, []*queryCase{
|
||||||
|
{url.Values{"query": {"art"}}, "q_art", false},
|
||||||
|
{url.Values{"query": {"alb"}}, "q_alb", false},
|
||||||
|
{url.Values{"query": {"tit"}}, "q_tra", false},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ctrlsubsonic
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@@ -80,7 +81,7 @@ func (c *Controller) ServeGetMusicFolders(r *http.Request) *spec.Response {
|
|||||||
|
|
||||||
func (c *Controller) ServeStartScan(r *http.Request) *spec.Response {
|
func (c *Controller) ServeStartScan(r *http.Request) *spec.Response {
|
||||||
go func() {
|
go func() {
|
||||||
if err := c.Scanner.Start(scanner.ScanOptions{}); err != nil {
|
if err := c.Scanner.ScanAndClean(scanner.ScanOptions{}); err != nil {
|
||||||
log.Printf("error while scanning: %v\n", err)
|
log.Printf("error while scanning: %v\n", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -95,7 +96,7 @@ func (c *Controller) ServeGetScanStatus(r *http.Request) *spec.Response {
|
|||||||
|
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.ScanStatus = &spec.ScanStatus{
|
sub.ScanStatus = &spec.ScanStatus{
|
||||||
Scanning: scanner.IsScanning(),
|
Scanning: c.Scanner.IsScanning(),
|
||||||
Count: trackCount,
|
Count: trackCount,
|
||||||
}
|
}
|
||||||
return sub
|
return sub
|
||||||
@@ -129,7 +130,7 @@ func (c *Controller) ServeGetPlayQueue(r *http.Request) *spec.Response {
|
|||||||
Where("user_id=?", user.ID).
|
Where("user_id=?", user.ID).
|
||||||
Find(&queue).
|
Find(&queue).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return spec.NewResponse()
|
return spec.NewResponse()
|
||||||
}
|
}
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
@@ -188,7 +189,7 @@ func (c *Controller) ServeGetSong(r *http.Request) *spec.Response {
|
|||||||
Preload("Album").
|
Preload("Album").
|
||||||
First(track).
|
First(track).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return spec.NewError(10, "couldn't find a track with that id")
|
return spec.NewError(10, "couldn't find a track with that id")
|
||||||
}
|
}
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ctrlsubsonic
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -33,7 +34,7 @@ func playlistRender(c *Controller, playlist *db.Playlist) *spec.Playlist {
|
|||||||
Preload("Album").
|
Preload("Album").
|
||||||
Find(&track).
|
Find(&track).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.Printf("wasn't able to find track with id %d", id)
|
log.Printf("wasn't able to find track with id %d", id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,7 @@ func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response {
|
|||||||
Where("id=?", playlistID).
|
Where("id=?", playlistID).
|
||||||
Find(&playlist).
|
Find(&playlist).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return spec.NewError(70, "playlist with id `%d` not found", playlistID)
|
return spec.NewError(70, "playlist with id `%d` not found", playlistID)
|
||||||
}
|
}
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *s
|
|||||||
_, err = os.Stat(cachePath)
|
_, err = os.Stat(cachePath)
|
||||||
switch {
|
switch {
|
||||||
case os.IsNotExist(err):
|
case os.IsNotExist(err):
|
||||||
coverPath, err := coverGetPath(c.DB, c.MusicPath, c.Podcasts.PodcastBasePath, id)
|
coverPath, err := coverGetPath(c.DB, c.PodcastsPath, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return spec.NewError(10, "couldn't find cover `%s`: %v", id, err)
|
return spec.NewError(10, "couldn't find cover `%s`: %v", id, err)
|
||||||
}
|
}
|
||||||
@@ -208,7 +208,7 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
|
|||||||
case specid.PodcastEpisode:
|
case specid.PodcastEpisode:
|
||||||
podcast, err := streamGetPodcast(c.DB, id.Value)
|
podcast, err := streamGetPodcast(c.DB, id.Value)
|
||||||
audioFile = podcast
|
audioFile = podcast
|
||||||
audioPath = path.Join(c.Podcasts.PodcastBasePath, podcast.Path)
|
audioPath = path.Join(c.PodcastsPath, podcast.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return spec.NewError(70, "podcast with id `%s` was not found", id)
|
return spec.NewError(70, "podcast with id `%s` was not found", id)
|
||||||
}
|
}
|
||||||
@@ -285,7 +285,7 @@ func (c *Controller) ServeDownload(w http.ResponseWriter, r *http.Request) *spec
|
|||||||
case specid.PodcastEpisode:
|
case specid.PodcastEpisode:
|
||||||
podcast, err := streamGetPodcast(c.DB, id.Value)
|
podcast, err := streamGetPodcast(c.DB, id.Value)
|
||||||
audioFile = podcast
|
audioFile = podcast
|
||||||
filePath = path.Join(c.Podcasts.PodcastBasePath, podcast.Path)
|
filePath = path.Join(c.PodcastsPath, podcast.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return spec.NewError(70, "podcast with id `%s` was not found", id)
|
return spec.NewError(70, "podcast with id `%s` was not found", id)
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
server/ctrlsubsonic/testdata/db
vendored
BIN
server/ctrlsubsonic/testdata/db
vendored
Binary file not shown.
@@ -6,133 +6,121 @@
|
|||||||
"albumList": {
|
"albumList": {
|
||||||
"album": [
|
"album": [
|
||||||
{
|
{
|
||||||
"id": "al-8",
|
"id": "al-2",
|
||||||
"coverArt": "al-8",
|
"coverArt": "al-2",
|
||||||
"artist": "13th Floor Lowervators",
|
"artist": "artist-0",
|
||||||
"title": "(1967) Easter Nowhere",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-7",
|
"parent": "al-1",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 10,
|
"songCount": 3,
|
||||||
"duration": 2609,
|
"duration": 300
|
||||||
"created": "2019-06-13T12:57:28.850090338+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-9",
|
|
||||||
"coverArt": "al-9",
|
|
||||||
"artist": "13th Floor Lowervators",
|
|
||||||
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-7",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 21,
|
|
||||||
"duration": 4222,
|
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-5",
|
|
||||||
"coverArt": "al-5",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "(1994) The Graveyard and the Ballroom",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-4",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 14,
|
|
||||||
"duration": 2738,
|
|
||||||
"created": "2019-06-05T17:46:37.675917974+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-6",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "(1981) To EachOTHER.",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-4",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 9,
|
|
||||||
"duration": 2801,
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-21",
|
|
||||||
"coverArt": "al-21",
|
|
||||||
"artist": "Captain Beefheart",
|
|
||||||
"title": "(1970) Lick My Decals Off, Bitch",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-20",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 15,
|
|
||||||
"duration": 2324,
|
|
||||||
"created": "2019-06-10T19:26:30.944742894+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-3",
|
"id": "al-3",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"artist": "Jah Wobble, The Edge, Holger Czukay",
|
"artist": "artist-0",
|
||||||
"title": "(1983) Snake Charmer",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-2",
|
"parent": "al-1",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 5,
|
"songCount": 3,
|
||||||
"duration": 1871,
|
"duration": 300
|
||||||
"created": "2019-05-16T22:10:52+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-16",
|
"id": "al-4",
|
||||||
"coverArt": "al-16",
|
"coverArt": "al-4",
|
||||||
"artist": "Swell Maps",
|
"artist": "artist-0",
|
||||||
"title": "(1980) Jane From Occupied Europe",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-15",
|
"parent": "al-1",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 16,
|
"songCount": 3,
|
||||||
"duration": 3040,
|
"duration": 300
|
||||||
"created": "2019-04-30T16:48:48+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-17",
|
"id": "al-7",
|
||||||
"coverArt": "al-17",
|
"coverArt": "al-7",
|
||||||
"artist": "Swell Maps",
|
"artist": "artist-1",
|
||||||
"title": "(1979) A Trip to Marineville",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-15",
|
"parent": "al-6",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 18,
|
"songCount": 3,
|
||||||
"duration": 3266,
|
"duration": 300
|
||||||
"created": "2019-04-30T16:48:48+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-19",
|
"id": "al-8",
|
||||||
"coverArt": "al-19",
|
"coverArt": "al-8",
|
||||||
"artist": "Ten Years After",
|
"artist": "artist-1",
|
||||||
"title": "(1967) Ten Years After",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-18",
|
"parent": "al-6",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 15,
|
"songCount": 3,
|
||||||
"duration": 3812,
|
"duration": 300
|
||||||
"created": "2019-04-30T16:48:30+01:00"
|
},
|
||||||
|
{
|
||||||
|
"id": "al-9",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-6",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-11",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-10",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-12",
|
||||||
|
"coverArt": "al-12",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-10",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-13",
|
"id": "al-13",
|
||||||
"coverArt": "al-13",
|
"coverArt": "al-13",
|
||||||
"artist": "There",
|
"artist": "artist-2",
|
||||||
"title": "(2010) Anika",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-12",
|
"parent": "al-10",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 9,
|
"songCount": 3,
|
||||||
"duration": 2169,
|
"duration": 300
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,133 +6,121 @@
|
|||||||
"albumList": {
|
"albumList": {
|
||||||
"album": [
|
"album": [
|
||||||
{
|
{
|
||||||
"id": "al-9",
|
"id": "al-2",
|
||||||
"coverArt": "al-9",
|
"coverArt": "al-2",
|
||||||
"artist": "13th Floor Lowervators",
|
"artist": "artist-0",
|
||||||
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-7",
|
"parent": "al-1",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 21,
|
"songCount": 3,
|
||||||
"duration": 4222,
|
"duration": 300
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-8",
|
"id": "al-7",
|
||||||
"coverArt": "al-8",
|
"coverArt": "al-7",
|
||||||
"artist": "13th Floor Lowervators",
|
"artist": "artist-1",
|
||||||
"title": "(1967) Easter Nowhere",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-7",
|
"parent": "al-6",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 10,
|
"songCount": 3,
|
||||||
"duration": 2609,
|
"duration": 300
|
||||||
"created": "2019-06-13T12:57:28.850090338+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-19",
|
"id": "al-11",
|
||||||
"coverArt": "al-19",
|
"coverArt": "al-11",
|
||||||
"artist": "Ten Years After",
|
"artist": "artist-2",
|
||||||
"title": "(1967) Ten Years After",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-18",
|
"parent": "al-10",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 15,
|
"songCount": 3,
|
||||||
"duration": 3812,
|
"duration": 300
|
||||||
"created": "2019-04-30T16:48:30+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-21",
|
|
||||||
"coverArt": "al-21",
|
|
||||||
"artist": "Captain Beefheart",
|
|
||||||
"title": "(1970) Lick My Decals Off, Bitch",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-20",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 15,
|
|
||||||
"duration": 2324,
|
|
||||||
"created": "2019-06-10T19:26:30.944742894+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-17",
|
|
||||||
"coverArt": "al-17",
|
|
||||||
"artist": "Swell Maps",
|
|
||||||
"title": "(1979) A Trip to Marineville",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-15",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 18,
|
|
||||||
"duration": 3266,
|
|
||||||
"created": "2019-04-30T16:48:48+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-16",
|
|
||||||
"coverArt": "al-16",
|
|
||||||
"artist": "Swell Maps",
|
|
||||||
"title": "(1980) Jane From Occupied Europe",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-15",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 16,
|
|
||||||
"duration": 3040,
|
|
||||||
"created": "2019-04-30T16:48:48+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-6",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "(1981) To EachOTHER.",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-4",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 9,
|
|
||||||
"duration": 2801,
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-3",
|
"id": "al-3",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"artist": "Jah Wobble, The Edge, Holger Czukay",
|
"artist": "artist-0",
|
||||||
"title": "(1983) Snake Charmer",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-2",
|
"parent": "al-1",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 5,
|
"songCount": 3,
|
||||||
"duration": 1871,
|
"duration": 300
|
||||||
"created": "2019-05-16T22:10:52+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-5",
|
"id": "al-8",
|
||||||
"coverArt": "al-5",
|
"coverArt": "al-8",
|
||||||
"artist": "A Certain Ratio",
|
"artist": "artist-1",
|
||||||
"title": "(1994) The Graveyard and the Ballroom",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-4",
|
"parent": "al-6",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 14,
|
"songCount": 3,
|
||||||
"duration": 2738,
|
"duration": 300
|
||||||
"created": "2019-06-05T17:46:37.675917974+01:00"
|
},
|
||||||
|
{
|
||||||
|
"id": "al-12",
|
||||||
|
"coverArt": "al-12",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-10",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-4",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-1",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-9",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-6",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-13",
|
"id": "al-13",
|
||||||
"coverArt": "al-13",
|
"coverArt": "al-13",
|
||||||
"artist": "There",
|
"artist": "artist-2",
|
||||||
"title": "(2010) Anika",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-12",
|
"parent": "al-10",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 9,
|
"songCount": 3,
|
||||||
"duration": 2169,
|
"duration": 300
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,133 +6,121 @@
|
|||||||
"albumList": {
|
"albumList": {
|
||||||
"album": [
|
"album": [
|
||||||
{
|
{
|
||||||
"id": "al-8",
|
"id": "al-2",
|
||||||
"coverArt": "al-8",
|
"coverArt": "al-2",
|
||||||
"artist": "13th Floor Lowervators",
|
"artist": "artist-0",
|
||||||
"title": "(1967) Easter Nowhere",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-7",
|
"parent": "al-1",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 10,
|
"songCount": 3,
|
||||||
"duration": 2609,
|
"duration": 300
|
||||||
"created": "2019-06-13T12:57:28.850090338+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-9",
|
|
||||||
"coverArt": "al-9",
|
|
||||||
"artist": "13th Floor Lowervators",
|
|
||||||
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-7",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 21,
|
|
||||||
"duration": 4222,
|
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-21",
|
|
||||||
"coverArt": "al-21",
|
|
||||||
"artist": "Captain Beefheart",
|
|
||||||
"title": "(1970) Lick My Decals Off, Bitch",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-20",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 15,
|
|
||||||
"duration": 2324,
|
|
||||||
"created": "2019-06-10T19:26:30.944742894+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-5",
|
|
||||||
"coverArt": "al-5",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "(1994) The Graveyard and the Ballroom",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-4",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 14,
|
|
||||||
"duration": 2738,
|
|
||||||
"created": "2019-06-05T17:46:37.675917974+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-6",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "(1981) To EachOTHER.",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-4",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 9,
|
|
||||||
"duration": 2801,
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-13",
|
|
||||||
"coverArt": "al-13",
|
|
||||||
"artist": "There",
|
|
||||||
"title": "(2010) Anika",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-12",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 9,
|
|
||||||
"duration": 2169,
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-3",
|
"id": "al-3",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"artist": "Jah Wobble, The Edge, Holger Czukay",
|
"artist": "artist-0",
|
||||||
"title": "(1983) Snake Charmer",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-2",
|
"parent": "al-1",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 5,
|
"songCount": 3,
|
||||||
"duration": 1871,
|
"duration": 300
|
||||||
"created": "2019-05-16T22:10:52+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-16",
|
"id": "al-4",
|
||||||
"coverArt": "al-16",
|
"coverArt": "al-4",
|
||||||
"artist": "Swell Maps",
|
"artist": "artist-0",
|
||||||
"title": "(1980) Jane From Occupied Europe",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-15",
|
"parent": "al-1",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 16,
|
"songCount": 3,
|
||||||
"duration": 3040,
|
"duration": 300
|
||||||
"created": "2019-04-30T16:48:48+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-17",
|
"id": "al-7",
|
||||||
"coverArt": "al-17",
|
"coverArt": "al-7",
|
||||||
"artist": "Swell Maps",
|
"artist": "artist-1",
|
||||||
"title": "(1979) A Trip to Marineville",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-15",
|
"parent": "al-6",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 18,
|
"songCount": 3,
|
||||||
"duration": 3266,
|
"duration": 300
|
||||||
"created": "2019-04-30T16:48:48+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-19",
|
"id": "al-8",
|
||||||
"coverArt": "al-19",
|
"coverArt": "al-8",
|
||||||
"artist": "Ten Years After",
|
"artist": "artist-1",
|
||||||
"title": "(1967) Ten Years After",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-18",
|
"parent": "al-6",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 15,
|
"songCount": 3,
|
||||||
"duration": 3812,
|
"duration": 300
|
||||||
"created": "2019-04-30T16:48:30+01:00"
|
},
|
||||||
|
{
|
||||||
|
"id": "al-9",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-6",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-11",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-10",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-12",
|
||||||
|
"coverArt": "al-12",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-10",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-13",
|
||||||
|
"coverArt": "al-13",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-10",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,133 +6,121 @@
|
|||||||
"albumList": {
|
"albumList": {
|
||||||
"album": [
|
"album": [
|
||||||
{
|
{
|
||||||
"id": "al-19",
|
"id": "al-2",
|
||||||
"coverArt": "al-19",
|
"coverArt": "al-2",
|
||||||
"artist": "Ten Years After",
|
"artist": "artist-0",
|
||||||
"title": "(1967) Ten Years After",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-18",
|
"parent": "al-1",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 15,
|
"songCount": 3,
|
||||||
"duration": 3812,
|
"duration": 300
|
||||||
"created": "2019-04-30T16:48:30+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-21",
|
"id": "al-7",
|
||||||
"coverArt": "al-21",
|
"coverArt": "al-7",
|
||||||
"artist": "Captain Beefheart",
|
"artist": "artist-1",
|
||||||
"title": "(1970) Lick My Decals Off, Bitch",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-20",
|
"parent": "al-6",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 15,
|
"songCount": 3,
|
||||||
"duration": 2324,
|
"duration": 300
|
||||||
"created": "2019-06-10T19:26:30.944742894+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-17",
|
|
||||||
"coverArt": "al-17",
|
|
||||||
"artist": "Swell Maps",
|
|
||||||
"title": "(1979) A Trip to Marineville",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-15",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 18,
|
|
||||||
"duration": 3266,
|
|
||||||
"created": "2019-04-30T16:48:48+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-6",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "(1981) To EachOTHER.",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-4",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 9,
|
|
||||||
"duration": 2801,
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-13",
|
"id": "al-13",
|
||||||
"coverArt": "al-13",
|
"coverArt": "al-13",
|
||||||
"artist": "There",
|
"artist": "artist-2",
|
||||||
"title": "(2010) Anika",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-12",
|
"parent": "al-10",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 9,
|
"songCount": 3,
|
||||||
"duration": 2169,
|
"duration": 300
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-8",
|
"id": "al-11",
|
||||||
"coverArt": "al-8",
|
"coverArt": "al-11",
|
||||||
"artist": "13th Floor Lowervators",
|
"artist": "artist-2",
|
||||||
"title": "(1967) Easter Nowhere",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-0",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-7",
|
"parent": "al-10",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 10,
|
"songCount": 3,
|
||||||
"duration": 2609,
|
"duration": 300
|
||||||
"created": "2019-06-13T12:57:28.850090338+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-9",
|
|
||||||
"coverArt": "al-9",
|
|
||||||
"artist": "13th Floor Lowervators",
|
|
||||||
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-7",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 21,
|
|
||||||
"duration": 4222,
|
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-5",
|
|
||||||
"coverArt": "al-5",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "(1994) The Graveyard and the Ballroom",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-4",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 14,
|
|
||||||
"duration": 2738,
|
|
||||||
"created": "2019-06-05T17:46:37.675917974+01:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-16",
|
|
||||||
"coverArt": "al-16",
|
|
||||||
"artist": "Swell Maps",
|
|
||||||
"title": "(1980) Jane From Occupied Europe",
|
|
||||||
"album": "",
|
|
||||||
"parent": "al-15",
|
|
||||||
"isDir": true,
|
|
||||||
"name": "",
|
|
||||||
"songCount": 16,
|
|
||||||
"duration": 3040,
|
|
||||||
"created": "2019-04-30T16:48:48+01:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-3",
|
"id": "al-3",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"artist": "Jah Wobble, The Edge, Holger Czukay",
|
"artist": "artist-0",
|
||||||
"title": "(1983) Snake Charmer",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
"album": "",
|
"album": "",
|
||||||
"parent": "al-2",
|
"parent": "al-1",
|
||||||
"isDir": true,
|
"isDir": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"songCount": 5,
|
"songCount": 3,
|
||||||
"duration": 1871,
|
"duration": 300
|
||||||
"created": "2019-05-16T22:10:52+01:00"
|
},
|
||||||
|
{
|
||||||
|
"id": "al-12",
|
||||||
|
"coverArt": "al-12",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-10",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-8",
|
||||||
|
"coverArt": "al-8",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-1",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-6",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-4",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-1",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-9",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "album-2",
|
||||||
|
"album": "",
|
||||||
|
"parent": "al-6",
|
||||||
|
"isDir": true,
|
||||||
|
"name": "",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,133 +6,121 @@
|
|||||||
"albumList2": {
|
"albumList2": {
|
||||||
"album": [
|
"album": [
|
||||||
{
|
{
|
||||||
"id": "al-8",
|
"id": "al-2",
|
||||||
"coverArt": "al-8",
|
"coverArt": "al-2",
|
||||||
"artistId": "ar-3",
|
"artistId": "ar-1",
|
||||||
"artist": "13th Floor Elevators",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Easter Everywhere",
|
"name": "album-0",
|
||||||
"songCount": 10,
|
"songCount": 3,
|
||||||
"duration": 2609,
|
"duration": 300,
|
||||||
"created": "2019-06-13T12:57:28.850090338+01:00",
|
"year": 2021
|
||||||
"year": 1967
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-9",
|
|
||||||
"coverArt": "al-9",
|
|
||||||
"artistId": "ar-3",
|
|
||||||
"artist": "13th Floor Elevators",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
|
||||||
"songCount": 21,
|
|
||||||
"duration": 4222,
|
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00",
|
|
||||||
"year": 1966
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-5",
|
|
||||||
"coverArt": "al-5",
|
|
||||||
"artistId": "ar-2",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "The Graveyard and the Ballroom",
|
|
||||||
"songCount": 14,
|
|
||||||
"duration": 2738,
|
|
||||||
"created": "2019-06-05T17:46:37.675917974+01:00",
|
|
||||||
"year": 1994
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-6",
|
|
||||||
"artistId": "ar-2",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "To Each...",
|
|
||||||
"songCount": 9,
|
|
||||||
"duration": 2801,
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
|
||||||
"year": 1981
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-13",
|
|
||||||
"coverArt": "al-13",
|
|
||||||
"artistId": "ar-4",
|
|
||||||
"artist": "Anikas",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Anika",
|
|
||||||
"songCount": 9,
|
|
||||||
"duration": 2169,
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
|
||||||
"year": 2010
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-21",
|
|
||||||
"coverArt": "al-21",
|
|
||||||
"artistId": "ar-7",
|
|
||||||
"artist": "Captain Beefheart & His Magic Band",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Lick My Decals Off, Baby",
|
|
||||||
"songCount": 15,
|
|
||||||
"duration": 2324,
|
|
||||||
"created": "2019-06-10T19:26:30.944742894+01:00",
|
|
||||||
"year": 1970
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-3",
|
"id": "al-3",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"artistId": "ar-1",
|
"artistId": "ar-1",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Snake Charmer",
|
"name": "album-1",
|
||||||
"songCount": 5,
|
"songCount": 3,
|
||||||
"duration": 1871,
|
"duration": 300,
|
||||||
"created": "2019-05-16T22:10:52+01:00",
|
"year": 2021
|
||||||
"year": 1983
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-16",
|
"id": "al-4",
|
||||||
"coverArt": "al-16",
|
"coverArt": "al-4",
|
||||||
"artistId": "ar-5",
|
"artistId": "ar-1",
|
||||||
"artist": "Swell Maps",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Jane From Occupied Europe",
|
"name": "album-2",
|
||||||
"songCount": 16,
|
"songCount": 3,
|
||||||
"duration": 3040,
|
"duration": 300,
|
||||||
"created": "2019-04-30T16:48:48+01:00",
|
"year": 2021
|
||||||
"year": 1980
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-17",
|
"id": "al-7",
|
||||||
"coverArt": "al-17",
|
"coverArt": "al-7",
|
||||||
"artistId": "ar-5",
|
"artistId": "ar-2",
|
||||||
"artist": "Swell Maps",
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "A Trip to Marineville",
|
"name": "album-0",
|
||||||
"songCount": 18,
|
"songCount": 3,
|
||||||
"duration": 3266,
|
"duration": 300,
|
||||||
"created": "2019-04-30T16:48:48+01:00",
|
"year": 2021
|
||||||
"year": 1979
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-19",
|
"id": "al-8",
|
||||||
"coverArt": "al-19",
|
"coverArt": "al-8",
|
||||||
"artistId": "ar-6",
|
"artistId": "ar-2",
|
||||||
"artist": "Ten Years After",
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Ten Years After",
|
"name": "album-1",
|
||||||
"songCount": 15,
|
"songCount": 3,
|
||||||
"duration": 3812,
|
"duration": 300,
|
||||||
"created": "2019-04-30T16:48:30+01:00",
|
"year": 2021
|
||||||
"year": 1967
|
},
|
||||||
|
{
|
||||||
|
"id": "al-9",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"artistId": "ar-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-11",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-0",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-12",
|
||||||
|
"coverArt": "al-12",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-1",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-13",
|
||||||
|
"coverArt": "al-13",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,133 +6,121 @@
|
|||||||
"albumList2": {
|
"albumList2": {
|
||||||
"album": [
|
"album": [
|
||||||
{
|
{
|
||||||
"id": "al-17",
|
"id": "al-2",
|
||||||
"coverArt": "al-17",
|
"coverArt": "al-2",
|
||||||
"artistId": "ar-5",
|
"artistId": "ar-1",
|
||||||
"artist": "Swell Maps",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "A Trip to Marineville",
|
"name": "album-0",
|
||||||
"songCount": 18,
|
"songCount": 3,
|
||||||
"duration": 3266,
|
"duration": 300,
|
||||||
"created": "2019-04-30T16:48:48+01:00",
|
"year": 2021
|
||||||
"year": 1979
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-13",
|
"id": "al-7",
|
||||||
"coverArt": "al-13",
|
"coverArt": "al-7",
|
||||||
"artistId": "ar-4",
|
"artistId": "ar-2",
|
||||||
"artist": "Anikas",
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Anika",
|
"name": "album-0",
|
||||||
"songCount": 9,
|
"songCount": 3,
|
||||||
"duration": 2169,
|
"duration": 300,
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
"year": 2021
|
||||||
"year": 2010
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-8",
|
"id": "al-11",
|
||||||
"coverArt": "al-8",
|
"coverArt": "al-11",
|
||||||
"artistId": "ar-3",
|
"artistId": "ar-3",
|
||||||
"artist": "13th Floor Elevators",
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Easter Everywhere",
|
"name": "album-0",
|
||||||
"songCount": 10,
|
"songCount": 3,
|
||||||
"duration": 2609,
|
"duration": 300,
|
||||||
"created": "2019-06-13T12:57:28.850090338+01:00",
|
"year": 2021
|
||||||
"year": 1967
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-16",
|
|
||||||
"coverArt": "al-16",
|
|
||||||
"artistId": "ar-5",
|
|
||||||
"artist": "Swell Maps",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Jane From Occupied Europe",
|
|
||||||
"songCount": 16,
|
|
||||||
"duration": 3040,
|
|
||||||
"created": "2019-04-30T16:48:48+01:00",
|
|
||||||
"year": 1980
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-21",
|
|
||||||
"coverArt": "al-21",
|
|
||||||
"artistId": "ar-7",
|
|
||||||
"artist": "Captain Beefheart & His Magic Band",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Lick My Decals Off, Baby",
|
|
||||||
"songCount": 15,
|
|
||||||
"duration": 2324,
|
|
||||||
"created": "2019-06-10T19:26:30.944742894+01:00",
|
|
||||||
"year": 1970
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-3",
|
"id": "al-3",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"artistId": "ar-1",
|
"artistId": "ar-1",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Snake Charmer",
|
"name": "album-1",
|
||||||
"songCount": 5,
|
"songCount": 3,
|
||||||
"duration": 1871,
|
"duration": 300,
|
||||||
"created": "2019-05-16T22:10:52+01:00",
|
"year": 2021
|
||||||
"year": 1983
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-19",
|
"id": "al-8",
|
||||||
"coverArt": "al-19",
|
"coverArt": "al-8",
|
||||||
"artistId": "ar-6",
|
|
||||||
"artist": "Ten Years After",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Ten Years After",
|
|
||||||
"songCount": 15,
|
|
||||||
"duration": 3812,
|
|
||||||
"created": "2019-04-30T16:48:30+01:00",
|
|
||||||
"year": 1967
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-5",
|
|
||||||
"coverArt": "al-5",
|
|
||||||
"artistId": "ar-2",
|
"artistId": "ar-2",
|
||||||
"artist": "A Certain Ratio",
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "The Graveyard and the Ballroom",
|
"name": "album-1",
|
||||||
"songCount": 14,
|
"songCount": 3,
|
||||||
"duration": 2738,
|
"duration": 300,
|
||||||
"created": "2019-06-05T17:46:37.675917974+01:00",
|
"year": 2021
|
||||||
"year": 1994
|
},
|
||||||
|
{
|
||||||
|
"id": "al-12",
|
||||||
|
"coverArt": "al-12",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-1",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-4",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-9",
|
"id": "al-9",
|
||||||
"coverArt": "al-9",
|
"coverArt": "al-9",
|
||||||
"artistId": "ar-3",
|
"artistId": "ar-2",
|
||||||
"artist": "13th Floor Elevators",
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
"name": "album-2",
|
||||||
"songCount": 21,
|
"songCount": 3,
|
||||||
"duration": 4222,
|
"duration": 300,
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00",
|
"year": 2021
|
||||||
"year": 1966
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-6",
|
"id": "al-13",
|
||||||
"artistId": "ar-2",
|
"coverArt": "al-13",
|
||||||
"artist": "A Certain Ratio",
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "To Each...",
|
"name": "album-2",
|
||||||
"songCount": 9,
|
"songCount": 3,
|
||||||
"duration": 2801,
|
"duration": 300,
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
"year": 2021
|
||||||
"year": 1981
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,133 +6,121 @@
|
|||||||
"albumList2": {
|
"albumList2": {
|
||||||
"album": [
|
"album": [
|
||||||
{
|
{
|
||||||
"id": "al-8",
|
"id": "al-2",
|
||||||
"coverArt": "al-8",
|
"coverArt": "al-2",
|
||||||
"artistId": "ar-3",
|
"artistId": "ar-1",
|
||||||
"artist": "13th Floor Elevators",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Easter Everywhere",
|
"name": "album-0",
|
||||||
"songCount": 10,
|
"songCount": 3,
|
||||||
"duration": 2609,
|
"duration": 300,
|
||||||
"created": "2019-06-13T12:57:28.850090338+01:00",
|
"year": 2021
|
||||||
"year": 1967
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-9",
|
|
||||||
"coverArt": "al-9",
|
|
||||||
"artistId": "ar-3",
|
|
||||||
"artist": "13th Floor Elevators",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
|
||||||
"songCount": 21,
|
|
||||||
"duration": 4222,
|
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00",
|
|
||||||
"year": 1966
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-21",
|
|
||||||
"coverArt": "al-21",
|
|
||||||
"artistId": "ar-7",
|
|
||||||
"artist": "Captain Beefheart & His Magic Band",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Lick My Decals Off, Baby",
|
|
||||||
"songCount": 15,
|
|
||||||
"duration": 2324,
|
|
||||||
"created": "2019-06-10T19:26:30.944742894+01:00",
|
|
||||||
"year": 1970
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-5",
|
|
||||||
"coverArt": "al-5",
|
|
||||||
"artistId": "ar-2",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "The Graveyard and the Ballroom",
|
|
||||||
"songCount": 14,
|
|
||||||
"duration": 2738,
|
|
||||||
"created": "2019-06-05T17:46:37.675917974+01:00",
|
|
||||||
"year": 1994
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-6",
|
|
||||||
"artistId": "ar-2",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "To Each...",
|
|
||||||
"songCount": 9,
|
|
||||||
"duration": 2801,
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
|
||||||
"year": 1981
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-13",
|
|
||||||
"coverArt": "al-13",
|
|
||||||
"artistId": "ar-4",
|
|
||||||
"artist": "Anikas",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Anika",
|
|
||||||
"songCount": 9,
|
|
||||||
"duration": 2169,
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
|
||||||
"year": 2010
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-3",
|
"id": "al-3",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"artistId": "ar-1",
|
"artistId": "ar-1",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Snake Charmer",
|
"name": "album-1",
|
||||||
"songCount": 5,
|
"songCount": 3,
|
||||||
"duration": 1871,
|
"duration": 300,
|
||||||
"created": "2019-05-16T22:10:52+01:00",
|
"year": 2021
|
||||||
"year": 1983
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-16",
|
"id": "al-4",
|
||||||
"coverArt": "al-16",
|
"coverArt": "al-4",
|
||||||
"artistId": "ar-5",
|
"artistId": "ar-1",
|
||||||
"artist": "Swell Maps",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Jane From Occupied Europe",
|
"name": "album-2",
|
||||||
"songCount": 16,
|
"songCount": 3,
|
||||||
"duration": 3040,
|
"duration": 300,
|
||||||
"created": "2019-04-30T16:48:48+01:00",
|
"year": 2021
|
||||||
"year": 1980
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-17",
|
"id": "al-7",
|
||||||
"coverArt": "al-17",
|
"coverArt": "al-7",
|
||||||
"artistId": "ar-5",
|
"artistId": "ar-2",
|
||||||
"artist": "Swell Maps",
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "A Trip to Marineville",
|
"name": "album-0",
|
||||||
"songCount": 18,
|
"songCount": 3,
|
||||||
"duration": 3266,
|
"duration": 300,
|
||||||
"created": "2019-04-30T16:48:48+01:00",
|
"year": 2021
|
||||||
"year": 1979
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-19",
|
"id": "al-8",
|
||||||
"coverArt": "al-19",
|
"coverArt": "al-8",
|
||||||
"artistId": "ar-6",
|
"artistId": "ar-2",
|
||||||
"artist": "Ten Years After",
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Ten Years After",
|
"name": "album-1",
|
||||||
"songCount": 15,
|
"songCount": 3,
|
||||||
"duration": 3812,
|
"duration": 300,
|
||||||
"created": "2019-04-30T16:48:30+01:00",
|
"year": 2021
|
||||||
"year": 1967
|
},
|
||||||
|
{
|
||||||
|
"id": "al-9",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"artistId": "ar-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-11",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-0",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-12",
|
||||||
|
"coverArt": "al-12",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-1",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-13",
|
||||||
|
"coverArt": "al-13",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,133 +6,121 @@
|
|||||||
"albumList2": {
|
"albumList2": {
|
||||||
"album": [
|
"album": [
|
||||||
{
|
{
|
||||||
"id": "al-17",
|
"id": "al-3",
|
||||||
"coverArt": "al-17",
|
"coverArt": "al-3",
|
||||||
"artistId": "ar-5",
|
"artistId": "ar-1",
|
||||||
"artist": "Swell Maps",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "A Trip to Marineville",
|
"name": "album-1",
|
||||||
"songCount": 18,
|
"songCount": 3,
|
||||||
"duration": 3266,
|
"duration": 300,
|
||||||
"created": "2019-04-30T16:48:48+01:00",
|
"year": 2021
|
||||||
"year": 1979
|
},
|
||||||
|
{
|
||||||
|
"id": "al-11",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-0",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-4",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-9",
|
"id": "al-9",
|
||||||
"coverArt": "al-9",
|
"coverArt": "al-9",
|
||||||
"artistId": "ar-3",
|
|
||||||
"artist": "13th Floor Elevators",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
|
||||||
"songCount": 21,
|
|
||||||
"duration": 4222,
|
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00",
|
|
||||||
"year": 1966
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-16",
|
|
||||||
"coverArt": "al-16",
|
|
||||||
"artistId": "ar-5",
|
|
||||||
"artist": "Swell Maps",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Jane From Occupied Europe",
|
|
||||||
"songCount": 16,
|
|
||||||
"duration": 3040,
|
|
||||||
"created": "2019-04-30T16:48:48+01:00",
|
|
||||||
"year": 1980
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-19",
|
|
||||||
"coverArt": "al-19",
|
|
||||||
"artistId": "ar-6",
|
|
||||||
"artist": "Ten Years After",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Ten Years After",
|
|
||||||
"songCount": 15,
|
|
||||||
"duration": 3812,
|
|
||||||
"created": "2019-04-30T16:48:30+01:00",
|
|
||||||
"year": 1967
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-6",
|
|
||||||
"artistId": "ar-2",
|
"artistId": "ar-2",
|
||||||
"artist": "A Certain Ratio",
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "To Each...",
|
"name": "album-2",
|
||||||
"songCount": 9,
|
"songCount": 3,
|
||||||
"duration": 2801,
|
"duration": 300,
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
"year": 2021
|
||||||
"year": 1981
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-8",
|
"id": "al-2",
|
||||||
"coverArt": "al-8",
|
"coverArt": "al-2",
|
||||||
"artistId": "ar-3",
|
|
||||||
"artist": "13th Floor Elevators",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Easter Everywhere",
|
|
||||||
"songCount": 10,
|
|
||||||
"duration": 2609,
|
|
||||||
"created": "2019-06-13T12:57:28.850090338+01:00",
|
|
||||||
"year": 1967
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-21",
|
|
||||||
"coverArt": "al-21",
|
|
||||||
"artistId": "ar-7",
|
|
||||||
"artist": "Captain Beefheart & His Magic Band",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Lick My Decals Off, Baby",
|
|
||||||
"songCount": 15,
|
|
||||||
"duration": 2324,
|
|
||||||
"created": "2019-06-10T19:26:30.944742894+01:00",
|
|
||||||
"year": 1970
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-5",
|
|
||||||
"coverArt": "al-5",
|
|
||||||
"artistId": "ar-2",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "The Graveyard and the Ballroom",
|
|
||||||
"songCount": 14,
|
|
||||||
"duration": 2738,
|
|
||||||
"created": "2019-06-05T17:46:37.675917974+01:00",
|
|
||||||
"year": 1994
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-3",
|
|
||||||
"coverArt": "al-3",
|
|
||||||
"artistId": "ar-1",
|
"artistId": "ar-1",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Snake Charmer",
|
"name": "album-0",
|
||||||
"songCount": 5,
|
"songCount": 3,
|
||||||
"duration": 1871,
|
"duration": 300,
|
||||||
"created": "2019-05-16T22:10:52+01:00",
|
"year": 2021
|
||||||
"year": 1983
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-13",
|
"id": "al-13",
|
||||||
"coverArt": "al-13",
|
"coverArt": "al-13",
|
||||||
"artistId": "ar-4",
|
"artistId": "ar-3",
|
||||||
"artist": "Anikas",
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Anika",
|
"name": "album-2",
|
||||||
"songCount": 9,
|
"songCount": 3,
|
||||||
"duration": 2169,
|
"duration": 300,
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
"year": 2021
|
||||||
"year": 2010
|
},
|
||||||
|
{
|
||||||
|
"id": "al-8",
|
||||||
|
"coverArt": "al-8",
|
||||||
|
"artistId": "ar-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-1",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-7",
|
||||||
|
"coverArt": "al-7",
|
||||||
|
"artistId": "ar-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-0",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-12",
|
||||||
|
"coverArt": "al-12",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-1",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,129 +7,80 @@
|
|||||||
"id": "al-3",
|
"id": "al-3",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"artistId": "ar-1",
|
"artistId": "ar-1",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Snake Charmer",
|
"name": "album-1",
|
||||||
"songCount": 5,
|
"songCount": 3,
|
||||||
"duration": 1871,
|
"duration": 300,
|
||||||
"created": "2019-05-16T22:10:52+01:00",
|
"year": 2021,
|
||||||
"year": 1983,
|
|
||||||
"song": [
|
"song": [
|
||||||
{
|
{
|
||||||
"id": "tr-1",
|
"id": "tr-4",
|
||||||
"album": "Snake Charmer",
|
"album": "album-1",
|
||||||
"albumId": "al-3",
|
"albumId": "al-3",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
"artistId": "ar-1",
|
"artistId": "ar-1",
|
||||||
"bitRate": 882,
|
"bitRate": 100,
|
||||||
"contentType": "audio/x-flac",
|
"contentType": "audio/x-flac",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"created": "2019-07-08T21:49:40.978045401+01:00",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"duration": 372,
|
"duration": 100,
|
||||||
"isDir": false,
|
"isDir": false,
|
||||||
"isVideo": false,
|
"isVideo": false,
|
||||||
"parent": "al-3",
|
"parent": "al-3",
|
||||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/01.05 Snake Charmer.flac",
|
"path": "artist-0/album-1/track-0.flac",
|
||||||
"size": 41274185,
|
|
||||||
"suffix": "flac",
|
"suffix": "flac",
|
||||||
"title": "Snake Charmer",
|
"title": "title-0",
|
||||||
"track": 1,
|
"track": 1,
|
||||||
"discNumber": 1,
|
"discNumber": 1,
|
||||||
"type": "music",
|
"type": "music",
|
||||||
"year": 1983
|
"year": 2021
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tr-3",
|
|
||||||
"album": "Snake Charmer",
|
|
||||||
"albumId": "al-3",
|
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
|
||||||
"artistId": "ar-1",
|
|
||||||
"bitRate": 814,
|
|
||||||
"contentType": "audio/x-flac",
|
|
||||||
"coverArt": "al-3",
|
|
||||||
"created": "2019-07-08T21:49:40.981605306+01:00",
|
|
||||||
"duration": 523,
|
|
||||||
"isDir": false,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-3",
|
|
||||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/02.05 Hold On to Your Dreams.flac",
|
|
||||||
"size": 53447545,
|
|
||||||
"suffix": "flac",
|
|
||||||
"title": "Hold On to Your Dreams",
|
|
||||||
"track": 2,
|
|
||||||
"discNumber": 1,
|
|
||||||
"type": "music",
|
|
||||||
"year": 1983
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tr-2",
|
|
||||||
"album": "Snake Charmer",
|
|
||||||
"albumId": "al-3",
|
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
|
||||||
"artistId": "ar-1",
|
|
||||||
"bitRate": 745,
|
|
||||||
"contentType": "audio/x-flac",
|
|
||||||
"coverArt": "al-3",
|
|
||||||
"created": "2019-07-08T21:49:40.979981084+01:00",
|
|
||||||
"duration": 331,
|
|
||||||
"isDir": false,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-3",
|
|
||||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/03.05 It Was a Camel.flac",
|
|
||||||
"size": 31080508,
|
|
||||||
"suffix": "flac",
|
|
||||||
"title": "It Was a Camel",
|
|
||||||
"track": 3,
|
|
||||||
"discNumber": 1,
|
|
||||||
"type": "music",
|
|
||||||
"year": 1983
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tr-5",
|
"id": "tr-5",
|
||||||
"album": "Snake Charmer",
|
"album": "album-1",
|
||||||
"albumId": "al-3",
|
"albumId": "al-3",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
"artistId": "ar-1",
|
"artistId": "ar-1",
|
||||||
"bitRate": 976,
|
"bitRate": 100,
|
||||||
"contentType": "audio/x-flac",
|
"contentType": "audio/x-flac",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"created": "2019-07-08T21:49:40.984853203+01:00",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"duration": 227,
|
"duration": 100,
|
||||||
"isDir": false,
|
"isDir": false,
|
||||||
"isVideo": false,
|
"isVideo": false,
|
||||||
"parent": "al-3",
|
"parent": "al-3",
|
||||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/04.05 Sleazy.flac",
|
"path": "artist-0/album-1/track-1.flac",
|
||||||
"size": 27938750,
|
|
||||||
"suffix": "flac",
|
"suffix": "flac",
|
||||||
"title": "Sleazy",
|
"title": "title-1",
|
||||||
"track": 4,
|
"track": 1,
|
||||||
"discNumber": 1,
|
"discNumber": 1,
|
||||||
"type": "music",
|
"type": "music",
|
||||||
"year": 1983
|
"year": 2021
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tr-4",
|
"id": "tr-6",
|
||||||
"album": "Snake Charmer",
|
"album": "album-1",
|
||||||
"albumId": "al-3",
|
"albumId": "al-3",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
"artistId": "ar-1",
|
"artistId": "ar-1",
|
||||||
"bitRate": 884,
|
"bitRate": 100,
|
||||||
"contentType": "audio/x-flac",
|
"contentType": "audio/x-flac",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"created": "2019-07-08T21:49:40.983301328+01:00",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"duration": 418,
|
"duration": 100,
|
||||||
"isDir": false,
|
"isDir": false,
|
||||||
"isVideo": false,
|
"isVideo": false,
|
||||||
"parent": "al-3",
|
"parent": "al-3",
|
||||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/05.05 Snake Charmer (reprise).flac",
|
"path": "artist-0/album-1/track-2.flac",
|
||||||
"size": 46427922,
|
|
||||||
"suffix": "flac",
|
"suffix": "flac",
|
||||||
"title": "Snake Charmer (reprise)",
|
"title": "title-2",
|
||||||
"track": 5,
|
"track": 1,
|
||||||
"discNumber": 1,
|
"discNumber": 1,
|
||||||
"type": "music",
|
"type": "music",
|
||||||
"year": 1983
|
"year": 2021
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,84 @@
|
|||||||
"type": "gonic",
|
"type": "gonic",
|
||||||
"album": {
|
"album": {
|
||||||
"id": "al-2",
|
"id": "al-2",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "",
|
"name": "album-0",
|
||||||
"songCount": 0,
|
"songCount": 3,
|
||||||
"duration": 0,
|
"duration": 300,
|
||||||
"created": "2019-05-16T22:10:21+01:00"
|
"year": 2021,
|
||||||
|
"song": [
|
||||||
|
{
|
||||||
|
"id": "tr-1",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-2",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-2",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-2",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-3",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-2",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,47 @@
|
|||||||
"type": "gonic",
|
"type": "gonic",
|
||||||
"artist": {
|
"artist": {
|
||||||
"id": "ar-1",
|
"id": "ar-1",
|
||||||
"name": "Jah Wobble, The Edge & Holger Czukay",
|
"name": "artist-0",
|
||||||
"albumCount": 1,
|
"albumCount": 3,
|
||||||
"album": [
|
"album": [
|
||||||
|
{
|
||||||
|
"id": "al-2",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-0",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "al-3",
|
"id": "al-3",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"artistId": "ar-1",
|
"artistId": "ar-1",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Snake Charmer",
|
"name": "album-1",
|
||||||
"songCount": 5,
|
"songCount": 3,
|
||||||
"duration": 1871,
|
"duration": 300,
|
||||||
"created": "2019-05-16T22:10:52+01:00",
|
"year": 2021
|
||||||
"year": 1983
|
},
|
||||||
|
{
|
||||||
|
"id": "al-4",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,34 +5,47 @@
|
|||||||
"type": "gonic",
|
"type": "gonic",
|
||||||
"artist": {
|
"artist": {
|
||||||
"id": "ar-3",
|
"id": "ar-3",
|
||||||
"name": "13th Floor Elevators",
|
"name": "artist-2",
|
||||||
"albumCount": 2,
|
"albumCount": 3,
|
||||||
"album": [
|
"album": [
|
||||||
{
|
{
|
||||||
"id": "al-8",
|
"id": "al-11",
|
||||||
"coverArt": "al-8",
|
"coverArt": "al-11",
|
||||||
"artistId": "ar-3",
|
"artistId": "ar-3",
|
||||||
"artist": "13th Floor Elevators",
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "Easter Everywhere",
|
"name": "album-0",
|
||||||
"songCount": 10,
|
"songCount": 3,
|
||||||
"duration": 2609,
|
"duration": 300,
|
||||||
"created": "2019-06-13T12:57:28.850090338+01:00",
|
"year": 2021
|
||||||
"year": 1967
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-9",
|
"id": "al-12",
|
||||||
"coverArt": "al-9",
|
"coverArt": "al-12",
|
||||||
"artistId": "ar-3",
|
"artistId": "ar-3",
|
||||||
"artist": "13th Floor Elevators",
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
"name": "album-1",
|
||||||
"songCount": 21,
|
"songCount": 3,
|
||||||
"duration": 4222,
|
"duration": 300,
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00",
|
"year": 2021
|
||||||
"year": 1966
|
},
|
||||||
|
{
|
||||||
|
"id": "al-13",
|
||||||
|
"coverArt": "al-13",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,33 +5,47 @@
|
|||||||
"type": "gonic",
|
"type": "gonic",
|
||||||
"artist": {
|
"artist": {
|
||||||
"id": "ar-2",
|
"id": "ar-2",
|
||||||
"name": "A Certain Ratio",
|
"name": "artist-1",
|
||||||
"albumCount": 2,
|
"albumCount": 3,
|
||||||
"album": [
|
"album": [
|
||||||
{
|
{
|
||||||
"id": "al-5",
|
"id": "al-7",
|
||||||
"coverArt": "al-5",
|
"coverArt": "al-7",
|
||||||
"artistId": "ar-2",
|
"artistId": "ar-2",
|
||||||
"artist": "A Certain Ratio",
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "The Graveyard and the Ballroom",
|
"name": "album-0",
|
||||||
"songCount": 14,
|
"songCount": 3,
|
||||||
"duration": 2738,
|
"duration": 300,
|
||||||
"created": "2019-06-05T17:46:37.675917974+01:00",
|
"year": 2021
|
||||||
"year": 1994
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "al-6",
|
"id": "al-8",
|
||||||
|
"coverArt": "al-8",
|
||||||
"artistId": "ar-2",
|
"artistId": "ar-2",
|
||||||
"artist": "A Certain Ratio",
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"title": "",
|
"title": "",
|
||||||
"album": "",
|
"album": "",
|
||||||
"name": "To Each...",
|
"name": "album-1",
|
||||||
"songCount": 9,
|
"songCount": 3,
|
||||||
"duration": 2801,
|
"duration": 300,
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
"year": 2021
|
||||||
"year": 1981
|
},
|
||||||
|
{
|
||||||
|
"id": "al-9",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"artistId": "ar-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 3,
|
||||||
|
"duration": 300,
|
||||||
|
"year": 2021
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,69 +6,12 @@
|
|||||||
"artists": {
|
"artists": {
|
||||||
"ignoredArticles": "",
|
"ignoredArticles": "",
|
||||||
"index": [
|
"index": [
|
||||||
{
|
|
||||||
"name": "#",
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "ar-3",
|
|
||||||
"name": "13th Floor Elevators",
|
|
||||||
"albumCount": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "a",
|
"name": "a",
|
||||||
"artist": [
|
"artist": [
|
||||||
{
|
{ "id": "ar-1", "name": "artist-0", "albumCount": 3 },
|
||||||
"id": "ar-2",
|
{ "id": "ar-2", "name": "artist-1", "albumCount": 3 },
|
||||||
"name": "A Certain Ratio",
|
{ "id": "ar-3", "name": "artist-2", "albumCount": 3 }
|
||||||
"albumCount": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ar-4",
|
|
||||||
"name": "Anikas",
|
|
||||||
"albumCount": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "c",
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "ar-7",
|
|
||||||
"name": "Captain Beefheart & His Magic Band",
|
|
||||||
"albumCount": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "j",
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "ar-1",
|
|
||||||
"name": "Jah Wobble, The Edge & Holger Czukay",
|
|
||||||
"albumCount": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "s",
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "ar-5",
|
|
||||||
"name": "Swell Maps",
|
|
||||||
"albumCount": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "t",
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "ar-6",
|
|
||||||
"name": "Ten Years After",
|
|
||||||
"albumCount": 1
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,69 +7,12 @@
|
|||||||
"lastModified": 0,
|
"lastModified": 0,
|
||||||
"ignoredArticles": "",
|
"ignoredArticles": "",
|
||||||
"index": [
|
"index": [
|
||||||
{
|
|
||||||
"name": "#",
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "al-7",
|
|
||||||
"name": "13th Floor Lowervators",
|
|
||||||
"albumCount": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "al-10",
|
|
||||||
"name": "___Anika",
|
|
||||||
"albumCount": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "a",
|
"name": "a",
|
||||||
"artist": [
|
"artist": [
|
||||||
{
|
{ "id": "al-2", "name": "album-0", "albumCount": 0 },
|
||||||
"id": "al-4",
|
{ "id": "al-3", "name": "album-1", "albumCount": 0 },
|
||||||
"name": "A Certain Ratio",
|
{ "id": "al-4", "name": "album-2", "albumCount": 0 }
|
||||||
"albumCount": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "c",
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "al-20",
|
|
||||||
"name": "Captain Beefheart",
|
|
||||||
"albumCount": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "j",
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "al-2",
|
|
||||||
"name": "Jah Wobble, The Edge, Holger Czukay",
|
|
||||||
"albumCount": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "s",
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "al-15",
|
|
||||||
"name": "Swell Maps",
|
|
||||||
"albumCount": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "t",
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "al-18",
|
|
||||||
"name": "Ten Years After",
|
|
||||||
"albumCount": 1
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,106 +5,62 @@
|
|||||||
"type": "gonic",
|
"type": "gonic",
|
||||||
"directory": {
|
"directory": {
|
||||||
"id": "al-3",
|
"id": "al-3",
|
||||||
"parent": "al-2",
|
"name": "album-1",
|
||||||
"name": "(1983) Snake Charmer",
|
|
||||||
"child": [
|
"child": [
|
||||||
{
|
{
|
||||||
"id": "tr-1",
|
"id": "tr-4",
|
||||||
"album": "(1983) Snake Charmer",
|
"album": "album-1",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
"bitRate": 882,
|
"bitRate": 100,
|
||||||
"contentType": "audio/x-flac",
|
"contentType": "audio/x-flac",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"created": "2019-07-08T21:49:40.978045401+01:00",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"duration": 372,
|
"duration": 100,
|
||||||
"isDir": false,
|
"isDir": false,
|
||||||
"isVideo": false,
|
"isVideo": false,
|
||||||
"parent": "al-3",
|
"parent": "al-3",
|
||||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/01.05 Snake Charmer.flac",
|
"path": "artist-0/album-1/track-0.flac",
|
||||||
"size": 41274185,
|
|
||||||
"suffix": "flac",
|
"suffix": "flac",
|
||||||
"title": "Snake Charmer",
|
"title": "title-0",
|
||||||
"track": 1,
|
"track": 1,
|
||||||
"discNumber": 1,
|
"discNumber": 1,
|
||||||
"type": "music"
|
"type": "music"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "tr-3",
|
|
||||||
"album": "(1983) Snake Charmer",
|
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
|
||||||
"bitRate": 814,
|
|
||||||
"contentType": "audio/x-flac",
|
|
||||||
"coverArt": "al-3",
|
|
||||||
"created": "2019-07-08T21:49:40.981605306+01:00",
|
|
||||||
"duration": 523,
|
|
||||||
"isDir": false,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-3",
|
|
||||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/02.05 Hold On to Your Dreams.flac",
|
|
||||||
"size": 53447545,
|
|
||||||
"suffix": "flac",
|
|
||||||
"title": "Hold On to Your Dreams",
|
|
||||||
"track": 2,
|
|
||||||
"discNumber": 1,
|
|
||||||
"type": "music"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tr-2",
|
|
||||||
"album": "(1983) Snake Charmer",
|
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
|
||||||
"bitRate": 745,
|
|
||||||
"contentType": "audio/x-flac",
|
|
||||||
"coverArt": "al-3",
|
|
||||||
"created": "2019-07-08T21:49:40.979981084+01:00",
|
|
||||||
"duration": 331,
|
|
||||||
"isDir": false,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-3",
|
|
||||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/03.05 It Was a Camel.flac",
|
|
||||||
"size": 31080508,
|
|
||||||
"suffix": "flac",
|
|
||||||
"title": "It Was a Camel",
|
|
||||||
"track": 3,
|
|
||||||
"discNumber": 1,
|
|
||||||
"type": "music"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "tr-5",
|
"id": "tr-5",
|
||||||
"album": "(1983) Snake Charmer",
|
"album": "album-1",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
"bitRate": 976,
|
"bitRate": 100,
|
||||||
"contentType": "audio/x-flac",
|
"contentType": "audio/x-flac",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"created": "2019-07-08T21:49:40.984853203+01:00",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"duration": 227,
|
"duration": 100,
|
||||||
"isDir": false,
|
"isDir": false,
|
||||||
"isVideo": false,
|
"isVideo": false,
|
||||||
"parent": "al-3",
|
"parent": "al-3",
|
||||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/04.05 Sleazy.flac",
|
"path": "artist-0/album-1/track-1.flac",
|
||||||
"size": 27938750,
|
|
||||||
"suffix": "flac",
|
"suffix": "flac",
|
||||||
"title": "Sleazy",
|
"title": "title-1",
|
||||||
"track": 4,
|
"track": 1,
|
||||||
"discNumber": 1,
|
"discNumber": 1,
|
||||||
"type": "music"
|
"type": "music"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tr-4",
|
"id": "tr-6",
|
||||||
"album": "(1983) Snake Charmer",
|
"album": "album-1",
|
||||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
"artist": "artist-0",
|
||||||
"bitRate": 884,
|
"bitRate": 100,
|
||||||
"contentType": "audio/x-flac",
|
"contentType": "audio/x-flac",
|
||||||
"coverArt": "al-3",
|
"coverArt": "al-3",
|
||||||
"created": "2019-07-08T21:49:40.983301328+01:00",
|
"created": "2019-11-30T00:00:00Z",
|
||||||
"duration": 418,
|
"duration": 100,
|
||||||
"isDir": false,
|
"isDir": false,
|
||||||
"isVideo": false,
|
"isVideo": false,
|
||||||
"parent": "al-3",
|
"parent": "al-3",
|
||||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/05.05 Snake Charmer (reprise).flac",
|
"path": "artist-0/album-1/track-2.flac",
|
||||||
"size": 46427922,
|
|
||||||
"suffix": "flac",
|
"suffix": "flac",
|
||||||
"title": "Snake Charmer (reprise)",
|
"title": "title-2",
|
||||||
"track": 5,
|
"track": 1,
|
||||||
"discNumber": 1,
|
"discNumber": 1,
|
||||||
"type": "music"
|
"type": "music"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,64 @@
|
|||||||
"type": "gonic",
|
"type": "gonic",
|
||||||
"directory": {
|
"directory": {
|
||||||
"id": "al-2",
|
"id": "al-2",
|
||||||
"name": "Jah Wobble, The Edge, Holger Czukay",
|
"name": "album-0",
|
||||||
"child": [
|
"child": [
|
||||||
{
|
{
|
||||||
"id": "al-3",
|
"id": "tr-1",
|
||||||
"coverArt": "al-3",
|
"album": "album-0",
|
||||||
"created": "2019-05-16T22:10:52+01:00",
|
"artist": "artist-0",
|
||||||
"isDir": true,
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
"isVideo": false,
|
"isVideo": false,
|
||||||
"parent": "al-2",
|
"parent": "al-2",
|
||||||
"title": "(1983) Snake Charmer"
|
"path": "artist-0/album-0/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-2",
|
||||||
|
"album": "album-0",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-3",
|
||||||
|
"album": "album-0",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"subsonic-response": {
|
|
||||||
"status": "ok",
|
|
||||||
"version": "1.15.0",
|
|
||||||
"type": "gonic",
|
|
||||||
"searchResult3": {
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "ar-3",
|
|
||||||
"name": "13th Floor Elevators",
|
|
||||||
"albumCount": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"album": [
|
|
||||||
{
|
|
||||||
"id": "al-9",
|
|
||||||
"coverArt": "al-9",
|
|
||||||
"artistId": "ar-3",
|
|
||||||
"artist": "13th Floor Elevators",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
|
||||||
"songCount": 0,
|
|
||||||
"duration": 0,
|
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00",
|
|
||||||
"year": 1966
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
128
server/ctrlsubsonic/testdata/test_search_three_q_alb
vendored
Normal file
128
server/ctrlsubsonic/testdata/test_search_three_q_alb
vendored
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"subsonic-response": {
|
||||||
|
"status": "ok",
|
||||||
|
"version": "1.15.0",
|
||||||
|
"type": "gonic",
|
||||||
|
"searchResult3": {
|
||||||
|
"album": [
|
||||||
|
{
|
||||||
|
"id": "al-2",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-0",
|
||||||
|
"songCount": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-3",
|
||||||
|
"coverArt": "al-3",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-1",
|
||||||
|
"songCount": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-4",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"artistId": "ar-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-7",
|
||||||
|
"coverArt": "al-7",
|
||||||
|
"artistId": "ar-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-0",
|
||||||
|
"songCount": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-8",
|
||||||
|
"coverArt": "al-8",
|
||||||
|
"artistId": "ar-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-1",
|
||||||
|
"songCount": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-9",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"artistId": "ar-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-11",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-0",
|
||||||
|
"songCount": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-12",
|
||||||
|
"coverArt": "al-12",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-1",
|
||||||
|
"songCount": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-13",
|
||||||
|
"coverArt": "al-13",
|
||||||
|
"artistId": "ar-3",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"title": "",
|
||||||
|
"album": "",
|
||||||
|
"name": "album-2",
|
||||||
|
"songCount": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"year": 2021
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"subsonic-response": {
|
|
||||||
"status": "ok",
|
|
||||||
"version": "1.15.0",
|
|
||||||
"type": "gonic",
|
|
||||||
"searchResult3": {
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "ar-4",
|
|
||||||
"name": "Anikas",
|
|
||||||
"albumCount": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"album": [
|
|
||||||
{
|
|
||||||
"id": "al-13",
|
|
||||||
"coverArt": "al-13",
|
|
||||||
"artistId": "ar-4",
|
|
||||||
"artist": "Anikas",
|
|
||||||
"title": "",
|
|
||||||
"album": "",
|
|
||||||
"name": "Anika",
|
|
||||||
"songCount": 0,
|
|
||||||
"duration": 0,
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
|
||||||
"year": 2010
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
server/ctrlsubsonic/testdata/test_search_three_q_art
vendored
Normal file
14
server/ctrlsubsonic/testdata/test_search_three_q_art
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"subsonic-response": {
|
||||||
|
"status": "ok",
|
||||||
|
"version": "1.15.0",
|
||||||
|
"type": "gonic",
|
||||||
|
"searchResult3": {
|
||||||
|
"artist": [
|
||||||
|
{ "id": "ar-1", "name": "artist-0", "albumCount": 0 },
|
||||||
|
{ "id": "ar-2", "name": "artist-1", "albumCount": 0 },
|
||||||
|
{ "id": "ar-3", "name": "artist-2", "albumCount": 0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"subsonic-response": {
|
|
||||||
"status": "ok",
|
|
||||||
"version": "1.15.0",
|
|
||||||
"type": "gonic",
|
|
||||||
"searchResult3": {
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "ar-2",
|
|
||||||
"name": "A Certain Ratio",
|
|
||||||
"albumCount": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
431
server/ctrlsubsonic/testdata/test_search_three_q_tra
vendored
Normal file
431
server/ctrlsubsonic/testdata/test_search_three_q_tra
vendored
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
{
|
||||||
|
"subsonic-response": {
|
||||||
|
"status": "ok",
|
||||||
|
"version": "1.15.0",
|
||||||
|
"type": "gonic",
|
||||||
|
"searchResult3": {
|
||||||
|
"song": [
|
||||||
|
{
|
||||||
|
"id": "tr-1",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-2",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-2",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-2",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-3",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-2",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-4",
|
||||||
|
"album": "album-1",
|
||||||
|
"albumId": "al-3",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-3",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-3",
|
||||||
|
"path": "artist-0/album-1/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-5",
|
||||||
|
"album": "album-1",
|
||||||
|
"albumId": "al-3",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-3",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-3",
|
||||||
|
"path": "artist-0/album-1/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-6",
|
||||||
|
"album": "album-1",
|
||||||
|
"albumId": "al-3",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-3",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-3",
|
||||||
|
"path": "artist-0/album-1/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-7",
|
||||||
|
"album": "album-2",
|
||||||
|
"albumId": "al-4",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-4",
|
||||||
|
"path": "artist-0/album-2/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-8",
|
||||||
|
"album": "album-2",
|
||||||
|
"albumId": "al-4",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-4",
|
||||||
|
"path": "artist-0/album-2/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-9",
|
||||||
|
"album": "album-2",
|
||||||
|
"albumId": "al-4",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-4",
|
||||||
|
"path": "artist-0/album-2/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-10",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-7",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-7",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-7",
|
||||||
|
"path": "artist-1/album-0/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-11",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-7",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-7",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-7",
|
||||||
|
"path": "artist-1/album-0/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-12",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-7",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-7",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-7",
|
||||||
|
"path": "artist-1/album-0/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-13",
|
||||||
|
"album": "album-1",
|
||||||
|
"albumId": "al-8",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-8",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-8",
|
||||||
|
"path": "artist-1/album-1/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-14",
|
||||||
|
"album": "album-1",
|
||||||
|
"albumId": "al-8",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-8",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-8",
|
||||||
|
"path": "artist-1/album-1/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-15",
|
||||||
|
"album": "album-1",
|
||||||
|
"albumId": "al-8",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-8",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-8",
|
||||||
|
"path": "artist-1/album-1/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-16",
|
||||||
|
"album": "album-2",
|
||||||
|
"albumId": "al-9",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-9",
|
||||||
|
"path": "artist-1/album-2/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-17",
|
||||||
|
"album": "album-2",
|
||||||
|
"albumId": "al-9",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-9",
|
||||||
|
"path": "artist-1/album-2/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-18",
|
||||||
|
"album": "album-2",
|
||||||
|
"albumId": "al-9",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-9",
|
||||||
|
"path": "artist-1/album-2/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-19",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-11",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-11",
|
||||||
|
"path": "artist-2/album-0/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-20",
|
||||||
|
"album": "album-0",
|
||||||
|
"albumId": "al-11",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-11",
|
||||||
|
"path": "artist-2/album-0/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music",
|
||||||
|
"year": 2021
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
148
server/ctrlsubsonic/testdata/test_search_two_q_13
vendored
148
server/ctrlsubsonic/testdata/test_search_two_q_13
vendored
@@ -1,148 +0,0 @@
|
|||||||
{
|
|
||||||
"subsonic-response": {
|
|
||||||
"status": "ok",
|
|
||||||
"version": "1.15.0",
|
|
||||||
"type": "gonic",
|
|
||||||
"searchResult2": {
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "al-7",
|
|
||||||
"name": "13th Floor Lowervators"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"album": [
|
|
||||||
{
|
|
||||||
"id": "al-9",
|
|
||||||
"coverArt": "al-9",
|
|
||||||
"created": "2019-06-13T12:57:24.306717554+01:00",
|
|
||||||
"isDir": true,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-7",
|
|
||||||
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"song": [
|
|
||||||
{
|
|
||||||
"id": "tr-6",
|
|
||||||
"album": "(1994) The Graveyard and the Ballroom",
|
|
||||||
"artist": "A Certain Ratio",
|
|
||||||
"bitRate": 894,
|
|
||||||
"contentType": "audio/x-flac",
|
|
||||||
"coverArt": "al-5",
|
|
||||||
"created": "2019-07-08T21:49:41.037683099+01:00",
|
|
||||||
"duration": 332,
|
|
||||||
"isDir": false,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-5",
|
|
||||||
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/13.14 Flight.flac",
|
|
||||||
"size": 37302417,
|
|
||||||
"suffix": "flac",
|
|
||||||
"title": "Flight",
|
|
||||||
"track": 13,
|
|
||||||
"discNumber": 1,
|
|
||||||
"type": "music"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tr-40",
|
|
||||||
"album": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
|
|
||||||
"artist": "13th Floor Elevators",
|
|
||||||
"bitRate": 244,
|
|
||||||
"contentType": "audio/mpeg",
|
|
||||||
"coverArt": "al-9",
|
|
||||||
"created": "2019-07-08T21:49:41.209108272+01:00",
|
|
||||||
"duration": 154,
|
|
||||||
"isDir": false,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-9",
|
|
||||||
"path": "13th Floor Lowervators/(1966) The Psychedelic Sounds of the 13th Floor Elevators/13.21 Before You Accuse Me.mp3",
|
|
||||||
"size": 4722688,
|
|
||||||
"suffix": "mp3",
|
|
||||||
"title": "Before You Accuse Me",
|
|
||||||
"track": 13,
|
|
||||||
"discNumber": 1,
|
|
||||||
"type": "music"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tr-76",
|
|
||||||
"album": "(1980) Jane From Occupied Europe",
|
|
||||||
"artist": "Swell Maps",
|
|
||||||
"bitRate": 1204,
|
|
||||||
"contentType": "audio/x-flac",
|
|
||||||
"coverArt": "al-16",
|
|
||||||
"created": "2019-07-08T21:49:41.43457798+01:00",
|
|
||||||
"duration": 220,
|
|
||||||
"isDir": false,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-16",
|
|
||||||
"path": "Swell Maps/(1980) Jane From Occupied Europe/13.16 Blenheim Shots.flac",
|
|
||||||
"size": 33140852,
|
|
||||||
"suffix": "flac",
|
|
||||||
"title": "Blenheim Shots",
|
|
||||||
"track": 13,
|
|
||||||
"discNumber": 1,
|
|
||||||
"type": "music"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tr-93",
|
|
||||||
"album": "(1979) A Trip to Marineville",
|
|
||||||
"artist": "Swell Maps",
|
|
||||||
"bitRate": 295,
|
|
||||||
"contentType": "audio/mpeg",
|
|
||||||
"coverArt": "al-17",
|
|
||||||
"created": "2019-07-08T21:49:41.493347193+01:00",
|
|
||||||
"duration": 463,
|
|
||||||
"isDir": false,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-17",
|
|
||||||
"path": "Swell Maps/(1979) A Trip to Marineville/d01 13.14 Adventuring Into Basketry.mp3",
|
|
||||||
"size": 17119309,
|
|
||||||
"suffix": "mp3",
|
|
||||||
"title": "Adventuring Into Basketry",
|
|
||||||
"track": 13,
|
|
||||||
"discNumber": 1,
|
|
||||||
"type": "music"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tr-107",
|
|
||||||
"album": "(1967) Ten Years After",
|
|
||||||
"artist": "Ten Years After",
|
|
||||||
"bitRate": 192,
|
|
||||||
"contentType": "audio/ogg",
|
|
||||||
"coverArt": "al-19",
|
|
||||||
"created": "2019-07-08T21:49:41.573811068+01:00",
|
|
||||||
"duration": 433,
|
|
||||||
"isDir": false,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-19",
|
|
||||||
"path": "Ten Years After/(1967) Ten Years After/13.15 Spider in My Web.ogg",
|
|
||||||
"size": 10400948,
|
|
||||||
"suffix": "ogg",
|
|
||||||
"title": "Spider in My Web",
|
|
||||||
"track": 13,
|
|
||||||
"discNumber": 1,
|
|
||||||
"type": "music"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tr-129",
|
|
||||||
"album": "(1970) Lick My Decals Off, Bitch",
|
|
||||||
"artist": "Captain Beefheart & His Magic Band",
|
|
||||||
"bitRate": 160,
|
|
||||||
"contentType": "audio/mpeg",
|
|
||||||
"coverArt": "al-21",
|
|
||||||
"created": "2019-07-08T21:49:41.687805489+01:00",
|
|
||||||
"duration": 152,
|
|
||||||
"isDir": false,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-21",
|
|
||||||
"path": "Captain Beefheart/(1970) Lick My Decals Off, Bitch/13.15 Space-Age Couple.mp3",
|
|
||||||
"size": 3054515,
|
|
||||||
"suffix": "mp3",
|
|
||||||
"title": "Space-Age Couple",
|
|
||||||
"track": 13,
|
|
||||||
"discNumber": 1,
|
|
||||||
"type": "music"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
97
server/ctrlsubsonic/testdata/test_search_two_q_alb
vendored
Normal file
97
server/ctrlsubsonic/testdata/test_search_two_q_alb
vendored
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"subsonic-response": {
|
||||||
|
"status": "ok",
|
||||||
|
"version": "1.15.0",
|
||||||
|
"type": "gonic",
|
||||||
|
"searchResult2": {
|
||||||
|
"artist": [
|
||||||
|
{ "id": "al-2", "name": "album-0" },
|
||||||
|
{ "id": "al-3", "name": "album-1" },
|
||||||
|
{ "id": "al-4", "name": "album-2" }
|
||||||
|
],
|
||||||
|
"album": [
|
||||||
|
{
|
||||||
|
"id": "al-2",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"isDir": true,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-1",
|
||||||
|
"title": "album-0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-3",
|
||||||
|
"coverArt": "al-3",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"isDir": true,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-1",
|
||||||
|
"title": "album-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-4",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"isDir": true,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-1",
|
||||||
|
"title": "album-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-7",
|
||||||
|
"coverArt": "al-7",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"isDir": true,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-6",
|
||||||
|
"title": "album-0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-8",
|
||||||
|
"coverArt": "al-8",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"isDir": true,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-6",
|
||||||
|
"title": "album-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-9",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"isDir": true,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-6",
|
||||||
|
"title": "album-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-11",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"isDir": true,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-10",
|
||||||
|
"title": "album-0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-12",
|
||||||
|
"coverArt": "al-12",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"isDir": true,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-10",
|
||||||
|
"title": "album-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "al-13",
|
||||||
|
"coverArt": "al-13",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"isDir": true,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-10",
|
||||||
|
"title": "album-2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"subsonic-response": {
|
|
||||||
"status": "ok",
|
|
||||||
"version": "1.15.0",
|
|
||||||
"type": "gonic",
|
|
||||||
"searchResult2": {
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "al-10",
|
|
||||||
"name": "___Anika"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"album": [
|
|
||||||
{
|
|
||||||
"id": "al-13",
|
|
||||||
"coverArt": "al-13",
|
|
||||||
"created": "2019-05-23T15:12:02.921473302+01:00",
|
|
||||||
"isDir": true,
|
|
||||||
"isVideo": false,
|
|
||||||
"parent": "al-12",
|
|
||||||
"title": "(2010) Anika"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
server/ctrlsubsonic/testdata/test_search_two_q_art
vendored
Normal file
8
server/ctrlsubsonic/testdata/test_search_two_q_art
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"subsonic-response": {
|
||||||
|
"status": "ok",
|
||||||
|
"version": "1.15.0",
|
||||||
|
"type": "gonic",
|
||||||
|
"searchResult2": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"subsonic-response": {
|
|
||||||
"status": "ok",
|
|
||||||
"version": "1.15.0",
|
|
||||||
"type": "gonic",
|
|
||||||
"searchResult2": {
|
|
||||||
"artist": [
|
|
||||||
{
|
|
||||||
"id": "al-4",
|
|
||||||
"name": "A Certain Ratio"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
391
server/ctrlsubsonic/testdata/test_search_two_q_tra
vendored
Normal file
391
server/ctrlsubsonic/testdata/test_search_two_q_tra
vendored
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
{
|
||||||
|
"subsonic-response": {
|
||||||
|
"status": "ok",
|
||||||
|
"version": "1.15.0",
|
||||||
|
"type": "gonic",
|
||||||
|
"searchResult2": {
|
||||||
|
"song": [
|
||||||
|
{
|
||||||
|
"id": "tr-1",
|
||||||
|
"album": "album-0",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-2",
|
||||||
|
"album": "album-0",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-3",
|
||||||
|
"album": "album-0",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-2",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-2",
|
||||||
|
"path": "artist-0/album-0/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-4",
|
||||||
|
"album": "album-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-3",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-3",
|
||||||
|
"path": "artist-0/album-1/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-5",
|
||||||
|
"album": "album-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-3",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-3",
|
||||||
|
"path": "artist-0/album-1/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-6",
|
||||||
|
"album": "album-1",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-3",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-3",
|
||||||
|
"path": "artist-0/album-1/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-7",
|
||||||
|
"album": "album-2",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-4",
|
||||||
|
"path": "artist-0/album-2/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-8",
|
||||||
|
"album": "album-2",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-4",
|
||||||
|
"path": "artist-0/album-2/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-9",
|
||||||
|
"album": "album-2",
|
||||||
|
"artist": "artist-0",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-4",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-4",
|
||||||
|
"path": "artist-0/album-2/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-10",
|
||||||
|
"album": "album-0",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-7",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-7",
|
||||||
|
"path": "artist-1/album-0/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-11",
|
||||||
|
"album": "album-0",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-7",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-7",
|
||||||
|
"path": "artist-1/album-0/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-12",
|
||||||
|
"album": "album-0",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-7",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-7",
|
||||||
|
"path": "artist-1/album-0/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-13",
|
||||||
|
"album": "album-1",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-8",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-8",
|
||||||
|
"path": "artist-1/album-1/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-14",
|
||||||
|
"album": "album-1",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-8",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-8",
|
||||||
|
"path": "artist-1/album-1/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-15",
|
||||||
|
"album": "album-1",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-8",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-8",
|
||||||
|
"path": "artist-1/album-1/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-16",
|
||||||
|
"album": "album-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-9",
|
||||||
|
"path": "artist-1/album-2/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-17",
|
||||||
|
"album": "album-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-9",
|
||||||
|
"path": "artist-1/album-2/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-18",
|
||||||
|
"album": "album-2",
|
||||||
|
"artist": "artist-1",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-9",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-9",
|
||||||
|
"path": "artist-1/album-2/track-2.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-2",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-19",
|
||||||
|
"album": "album-0",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-11",
|
||||||
|
"path": "artist-2/album-0/track-0.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-0",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tr-20",
|
||||||
|
"album": "album-0",
|
||||||
|
"artist": "artist-2",
|
||||||
|
"bitRate": 100,
|
||||||
|
"contentType": "audio/x-flac",
|
||||||
|
"coverArt": "al-11",
|
||||||
|
"created": "2019-11-30T00:00:00Z",
|
||||||
|
"duration": 100,
|
||||||
|
"isDir": false,
|
||||||
|
"isVideo": false,
|
||||||
|
"parent": "al-11",
|
||||||
|
"path": "artist-2/album-0/track-1.flac",
|
||||||
|
"suffix": "flac",
|
||||||
|
"title": "title-1",
|
||||||
|
"track": 1,
|
||||||
|
"discNumber": 1,
|
||||||
|
"type": "music"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +1,19 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
"gopkg.in/gormigrate.v1"
|
"gopkg.in/gormigrate.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// wrapMigrations wraps a list of migrations to add logging and transactions
|
func DefaultOptions() url.Values {
|
||||||
func wrapMigrations(migrs ...gormigrate.Migration) []*gormigrate.Migration {
|
|
||||||
log := func(i int, mig gormigrate.MigrateFunc, name string) gormigrate.MigrateFunc {
|
|
||||||
return func(db *gorm.DB) error {
|
|
||||||
// print that we're on the ith out of n migrations
|
|
||||||
defer log.Printf("migration (%d/%d) '%s' finished", i+1, len(migrs), name)
|
|
||||||
return db.Transaction(mig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret := make([]*gormigrate.Migration, 0, len(migrs))
|
|
||||||
for i, mig := range migrs {
|
|
||||||
ret = append(ret, &gormigrate.Migration{
|
|
||||||
ID: mig.ID,
|
|
||||||
Rollback: mig.Rollback,
|
|
||||||
Migrate: log(i, mig.Migrate, mig.ID),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultOptions() url.Values {
|
|
||||||
return url.Values{
|
return url.Values{
|
||||||
// with this, multiple connections share a single data and schema cache.
|
// with this, multiple connections share a single data and schema cache.
|
||||||
// see https://www.sqlite.org/sharedcache.html
|
// see https://www.sqlite.org/sharedcache.html
|
||||||
@@ -46,17 +26,23 @@ func defaultOptions() url.Values {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mockOptions() url.Values {
|
||||||
|
return url.Values{
|
||||||
|
"_foreign_keys": {"true"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type DB struct {
|
type DB struct {
|
||||||
*gorm.DB
|
*gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(path string) (*DB, error) {
|
func New(path string, options url.Values) (*DB, error) {
|
||||||
// https://github.com/mattn/go-sqlite3#connection-string
|
// https://github.com/mattn/go-sqlite3#connection-string
|
||||||
url := url.URL{
|
url := url.URL{
|
||||||
Scheme: "file",
|
Scheme: "file",
|
||||||
Opaque: path,
|
Opaque: path,
|
||||||
}
|
}
|
||||||
url.RawQuery = defaultOptions().Encode()
|
url.RawQuery = options.Encode()
|
||||||
db, err := gorm.Open("sqlite3", url.String())
|
db, err := gorm.Open("sqlite3", url.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("with gorm: %w", err)
|
return nil, fmt.Errorf("with gorm: %w", err)
|
||||||
@@ -91,34 +77,29 @@ func New(path string) (*DB, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewMock() (*DB, error) {
|
func NewMock() (*DB, error) {
|
||||||
return New(":memory:")
|
return New(":memory:", mockOptions())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) GetSetting(key string) string {
|
func (db *DB) GetSetting(key string) (string, error) {
|
||||||
setting := &Setting{}
|
setting := &Setting{}
|
||||||
db.
|
if err := db.Where("key=?", key).First(setting).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
Where("key=?", key).
|
return "", err
|
||||||
First(setting)
|
}
|
||||||
return setting.Value
|
return setting.Value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) SetSetting(key, value string) {
|
func (db *DB) SetSetting(key, value string) error {
|
||||||
db.
|
return db.
|
||||||
Where(Setting{Key: key}).
|
Where(Setting{Key: key}).
|
||||||
Assign(Setting{Value: value}).
|
Assign(Setting{Value: value}).
|
||||||
FirstOrCreate(&Setting{})
|
FirstOrCreate(&Setting{}).
|
||||||
}
|
Error
|
||||||
|
|
||||||
func (db *DB) GetOrCreateKey(key string) string {
|
|
||||||
value := db.GetSetting(key)
|
|
||||||
if value == "" {
|
|
||||||
value = string(securecookie.GenerateRandomKey(32))
|
|
||||||
db.SetSetting(key, value)
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) InsertBulkLeftMany(table string, head []string, left int, col []int) error {
|
func (db *DB) InsertBulkLeftMany(table string, head []string, left int, col []int) error {
|
||||||
|
if len(col) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var rows []string
|
var rows []string
|
||||||
var values []interface{}
|
var values []interface{}
|
||||||
for _, c := range col {
|
for _, c := range col {
|
||||||
@@ -139,7 +120,7 @@ func (db *DB) GetUserByID(id int) *User {
|
|||||||
Where("id=?", id).
|
Where("id=?", id).
|
||||||
First(user).
|
First(user).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
@@ -151,7 +132,7 @@ func (db *DB) GetUserByName(name string) *User {
|
|||||||
Where("name=?", name).
|
Where("name=?", name).
|
||||||
First(user).
|
First(user).
|
||||||
Error
|
Error
|
||||||
if gorm.IsRecordNotFoundError(err) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
@@ -164,6 +145,9 @@ func (db *DB) Begin() *DB {
|
|||||||
type ChunkFunc func(*gorm.DB, []int64) error
|
type ChunkFunc func(*gorm.DB, []int64) error
|
||||||
|
|
||||||
func (db *DB) TransactionChunked(data []int64, cb ChunkFunc) error {
|
func (db *DB) TransactionChunked(data []int64, cb ChunkFunc) error {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
// https://sqlite.org/limits.html
|
// https://sqlite.org/limits.html
|
||||||
const size = 999
|
const size = 999
|
||||||
return db.Transaction(func(tx *gorm.DB) error {
|
return db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
|
"github.com/matryer/is"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testDB *DB
|
|
||||||
|
|
||||||
func randKey() string {
|
func randKey() string {
|
||||||
letters := []rune("abcdef0123456789")
|
letters := []rune("abcdef0123456789")
|
||||||
b := make([]rune, 16)
|
b := make([]rune, 16)
|
||||||
@@ -22,27 +22,31 @@ func randKey() string {
|
|||||||
|
|
||||||
func TestGetSetting(t *testing.T) {
|
func TestGetSetting(t *testing.T) {
|
||||||
key := randKey()
|
key := randKey()
|
||||||
// new key
|
value := "howdy"
|
||||||
expected := "hello"
|
|
||||||
testDB.SetSetting(key, expected)
|
is := is.New(t)
|
||||||
actual := testDB.GetSetting(key)
|
|
||||||
if actual != expected {
|
testDB, err := NewMock()
|
||||||
t.Errorf("expected %q, got %q", expected, actual)
|
if err != nil {
|
||||||
|
t.Fatalf("error creating db: %v", err)
|
||||||
}
|
}
|
||||||
// existing key
|
if err := testDB.Migrate(MigrationContext{}); err != nil {
|
||||||
expected = "howdy"
|
t.Fatalf("error migrating db: %v", err)
|
||||||
testDB.SetSetting(key, expected)
|
|
||||||
actual = testDB.GetSetting(key)
|
|
||||||
if actual != expected {
|
|
||||||
t.Errorf("expected %q, got %q", expected, actual)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is.NoErr(testDB.SetSetting(key, value))
|
||||||
|
|
||||||
|
actual, err := testDB.GetSetting(key)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(actual, value)
|
||||||
|
|
||||||
|
is.NoErr(testDB.SetSetting(key, value))
|
||||||
|
actual, err = testDB.GetSetting(key)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(actual, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
var err error
|
log.SetOutput(ioutil.Discard)
|
||||||
testDB, err = NewMock()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error opening database: %v\n", err)
|
|
||||||
}
|
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
@@ -31,20 +32,14 @@ func migrateInitSchema() gormigrate.Migration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateCreateInitUser() gormigrate.Migration {
|
func construct(ctx MigrationContext, id string, f func(*gorm.DB, MigrationContext) error) *gormigrate.Migration {
|
||||||
return gormigrate.Migration{
|
return &gormigrate.Migration{
|
||||||
ID: "202002192019",
|
ID: id,
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(db *gorm.DB) error {
|
||||||
const (
|
tx := db.Begin()
|
||||||
initUsername = "admin"
|
defer tx.Commit()
|
||||||
initPassword = "admin"
|
if err := f(tx, ctx); err != nil {
|
||||||
)
|
return fmt.Errorf("%q: %w", id, err)
|
||||||
err := tx.
|
|
||||||
Where("name=?", initUsername).
|
|
||||||
First(&User{}).
|
|
||||||
Error
|
|
||||||
if !gorm.IsRecordNotFoundError(err) {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tx.Create(&User{
|
return tx.Create(&User{
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ type Track struct {
|
|||||||
Artist *Artist
|
Artist *Artist
|
||||||
ArtistID int `gorm:"not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
|
ArtistID int `gorm:"not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
|
||||||
Genres []*Genre `gorm:"many2many:track_genres"`
|
Genres []*Genre `gorm:"many2many:track_genres"`
|
||||||
Size int `gorm:"not null" sql:"default: null"`
|
Size int `sql:"default: null"`
|
||||||
Length int `sql:"default: null"`
|
Length int `sql:"default: null"`
|
||||||
Bitrate int `sql:"default: null"`
|
Bitrate int `sql:"default: null"`
|
||||||
TagTitle string `sql:"default: null"`
|
TagTitle string `sql:"default: null"`
|
||||||
|
|||||||
269
server/mockfs/mockfs.go
Normal file
269
server/mockfs/mockfs.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package mockfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.senan.xyz/gonic/server/db"
|
||||||
|
"go.senan.xyz/gonic/server/scanner"
|
||||||
|
"go.senan.xyz/gonic/server/scanner/tags"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrPathNotFound = errors.New("path not found")
|
||||||
|
|
||||||
|
type MockFS struct {
|
||||||
|
t *testing.T
|
||||||
|
scanner *scanner.Scanner
|
||||||
|
dir string
|
||||||
|
reader *mreader
|
||||||
|
db *db.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(t *testing.T) *MockFS {
|
||||||
|
return new(t, []string{""})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWithDirs(t *testing.T, dirs []string) *MockFS {
|
||||||
|
return new(t, dirs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func new(t *testing.T, dirs []string) *MockFS {
|
||||||
|
dbc, err := db.NewMock()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create db: %v", err)
|
||||||
|
}
|
||||||
|
if err := dbc.Migrate(db.MigrationContext{}); err != nil {
|
||||||
|
t.Fatalf("migrate db db: %v", err)
|
||||||
|
}
|
||||||
|
dbc.LogMode(false)
|
||||||
|
|
||||||
|
tmpDir, err := ioutil.TempDir("", "gonic-test-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create tmp dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var absDirs []string
|
||||||
|
for _, dir := range dirs {
|
||||||
|
absDirs = append(absDirs, filepath.Join(tmpDir, dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := &mreader{map[string]*Tags{}}
|
||||||
|
scanner := scanner.New(absDirs, true, dbc, ";", parser)
|
||||||
|
|
||||||
|
return &MockFS{
|
||||||
|
t: t,
|
||||||
|
scanner: scanner,
|
||||||
|
dir: tmpDir,
|
||||||
|
reader: parser,
|
||||||
|
db: dbc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) DB() *db.DB { return m.db }
|
||||||
|
func (m *MockFS) TmpDir() string { return m.dir }
|
||||||
|
|
||||||
|
func (m *MockFS) ScanAndClean() {
|
||||||
|
if err := m.scanner.ScanAndClean(scanner.ScanOptions{}); err != nil {
|
||||||
|
m.t.Fatalf("error scan and cleaning: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) ResetDates() {
|
||||||
|
t := time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC)
|
||||||
|
if err := m.db.Model(db.Album{}).Updates(db.Album{CreatedAt: t, UpdatedAt: t, ModifiedAt: t}).Error; err != nil {
|
||||||
|
m.t.Fatalf("reset album times: %v", err)
|
||||||
|
}
|
||||||
|
if err := m.db.Model(db.Track{}).Updates(db.Track{CreatedAt: t, UpdatedAt: t}).Error; err != nil {
|
||||||
|
m.t.Fatalf("reset track times: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) CleanUp() {
|
||||||
|
if err := m.db.Close(); err != nil {
|
||||||
|
m.t.Fatalf("close db: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(m.dir); err != nil {
|
||||||
|
m.t.Fatalf("remove all: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) addItems(prefix string, covers bool) {
|
||||||
|
p := func(format string, a ...interface{}) string {
|
||||||
|
return filepath.Join(prefix, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
for ar := 0; ar < 3; ar++ {
|
||||||
|
for al := 0; al < 3; al++ {
|
||||||
|
for tr := 0; tr < 3; tr++ {
|
||||||
|
m.AddTrack(p("artist-%d/album-%d/track-%d.flac", ar, al, tr))
|
||||||
|
m.SetTags(p("artist-%d/album-%d/track-%d.flac", ar, al, tr), func(tags *Tags) {
|
||||||
|
tags.RawArtist = fmt.Sprintf("artist-%d", ar)
|
||||||
|
tags.RawAlbumArtist = fmt.Sprintf("artist-%d", ar)
|
||||||
|
tags.RawAlbum = fmt.Sprintf("album-%d", al)
|
||||||
|
tags.RawTitle = fmt.Sprintf("title-%d", tr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if covers {
|
||||||
|
m.AddCover(p("artist-%d/album-%d/cover.png", ar, al))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) AddItems() { m.addItems("", false) }
|
||||||
|
func (m *MockFS) AddItemsPrefix(prefix string) { m.addItems(prefix, false) }
|
||||||
|
func (m *MockFS) AddItemsWithCovers() { m.addItems("", true) }
|
||||||
|
func (m *MockFS) AddItemsPrefixWithCovers(prefix string) { m.addItems(prefix, true) }
|
||||||
|
|
||||||
|
func (m *MockFS) RemoveAll(path string) {
|
||||||
|
abspath := filepath.Join(m.dir, path)
|
||||||
|
if err := os.RemoveAll(abspath); err != nil {
|
||||||
|
m.t.Fatalf("remove all: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) LogItems() {
|
||||||
|
var dirs int
|
||||||
|
err := filepath.Walk(m.dir, func(path string, info fs.FileInfo, err error) error {
|
||||||
|
m.t.Logf("item %q", path)
|
||||||
|
if info.IsDir() {
|
||||||
|
dirs++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
m.t.Fatalf("error logging items: %v", err)
|
||||||
|
}
|
||||||
|
m.t.Logf("dirs: %d", dirs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) LogAlbums() {
|
||||||
|
var albums []*db.Album
|
||||||
|
if err := m.db.Find(&albums).Error; err != nil {
|
||||||
|
m.t.Fatalf("error logging items: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.t.Logf("\nalbums")
|
||||||
|
for _, album := range albums {
|
||||||
|
m.t.Logf("id %-3d root %-3s %-10s %-10s pid %-3d aid %-3d cov %-10s",
|
||||||
|
album.ID, album.RootDir, album.LeftPath, album.RightPath, album.ParentID, album.TagArtistID, album.Cover)
|
||||||
|
}
|
||||||
|
m.t.Logf("total %d", len(albums))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) LogArtists() {
|
||||||
|
var artists []*db.Artist
|
||||||
|
if err := m.db.Find(&artists).Error; err != nil {
|
||||||
|
m.t.Fatalf("error logging items: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.t.Logf("\nartists")
|
||||||
|
for _, artist := range artists {
|
||||||
|
m.t.Logf("id %-3d %-10s", artist.ID, artist.Name)
|
||||||
|
}
|
||||||
|
m.t.Logf("total %d", len(artists))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) LogTracks() {
|
||||||
|
var tracks []*db.Track
|
||||||
|
if err := m.db.Find(&tracks).Error; err != nil {
|
||||||
|
m.t.Fatalf("error logging items: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.t.Logf("\ntracks")
|
||||||
|
for _, track := range tracks {
|
||||||
|
m.t.Logf("id %-3d aid %-3d filename %-10s tagtitle %-10s",
|
||||||
|
track.ID, track.AlbumID, track.Filename, track.TagTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) LogTrackGenres() {
|
||||||
|
var tgs []*db.TrackGenre
|
||||||
|
if err := m.db.Find(&tgs).Error; err != nil {
|
||||||
|
m.t.Fatalf("error logging items: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.t.Logf("\ntrack genres")
|
||||||
|
for _, tg := range tgs {
|
||||||
|
m.t.Logf("tid %-3d gid %-3d", tg.TrackID, tg.GenreID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) AddTrack(path string) {
|
||||||
|
abspath := filepath.Join(m.dir, path)
|
||||||
|
dir := filepath.Dir(abspath)
|
||||||
|
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
|
m.t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Create(abspath); err != nil {
|
||||||
|
m.t.Fatalf("create track: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) AddCover(path string) {
|
||||||
|
abspath := filepath.Join(m.dir, path)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(abspath), os.ModePerm); err != nil {
|
||||||
|
m.t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Create(abspath); err != nil {
|
||||||
|
m.t.Fatalf("create cover: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFS) SetTags(path string, cb func(*Tags)) {
|
||||||
|
abspath := filepath.Join(m.dir, path)
|
||||||
|
if err := os.Chtimes(abspath, time.Time{}, time.Now()); err != nil {
|
||||||
|
m.t.Fatalf("touch track: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := m.reader.tags[abspath]; !ok {
|
||||||
|
m.reader.tags[abspath] = &Tags{}
|
||||||
|
}
|
||||||
|
cb(m.reader.tags[abspath])
|
||||||
|
}
|
||||||
|
|
||||||
|
type mreader struct {
|
||||||
|
tags map[string]*Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mreader) Read(abspath string) (tags.Parser, error) {
|
||||||
|
parser, ok := m.tags[abspath]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrPathNotFound
|
||||||
|
}
|
||||||
|
return parser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ tags.Reader = (*mreader)(nil)
|
||||||
|
|
||||||
|
type Tags struct {
|
||||||
|
RawTitle string
|
||||||
|
RawArtist string
|
||||||
|
RawAlbum string
|
||||||
|
RawAlbumArtist string
|
||||||
|
RawGenre string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Tags) Title() string { return m.RawTitle }
|
||||||
|
func (m *Tags) BrainzID() string { return "" }
|
||||||
|
func (m *Tags) Artist() string { return m.RawArtist }
|
||||||
|
func (m *Tags) Album() string { return m.RawAlbum }
|
||||||
|
func (m *Tags) AlbumArtist() string { return m.RawAlbumArtist }
|
||||||
|
func (m *Tags) AlbumBrainzID() string { return "" }
|
||||||
|
func (m *Tags) Genre() string { return m.RawGenre }
|
||||||
|
func (m *Tags) TrackNumber() int { return 1 }
|
||||||
|
func (m *Tags) DiscNumber() int { return 1 }
|
||||||
|
func (m *Tags) Length() int { return 100 }
|
||||||
|
func (m *Tags) Bitrate() int { return 100 }
|
||||||
|
func (m *Tags) Year() int { return 2021 }
|
||||||
|
|
||||||
|
func (m *Tags) SomeAlbum() string { return m.Album() }
|
||||||
|
func (m *Tags) SomeArtist() string { return m.Artist() }
|
||||||
|
func (m *Tags) SomeAlbumArtist() string { return m.AlbumArtist() }
|
||||||
|
func (m *Tags) SomeGenre() string { return m.Genre() }
|
||||||
|
|
||||||
|
var _ tags.Parser = (*Tags)(nil)
|
||||||
@@ -24,16 +24,25 @@ import (
|
|||||||
"go.senan.xyz/gonic/server/scanner/tags"
|
"go.senan.xyz/gonic/server/scanner/tags"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DownloadAllWaitInterval = 3 * time.Second
|
const downloadAllWaitInterval = 3 * time.Second
|
||||||
|
|
||||||
type Podcasts struct {
|
type Podcasts struct {
|
||||||
DB *db.DB
|
db *db.DB
|
||||||
PodcastBasePath string
|
baseDir string
|
||||||
|
tagger tags.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *db.DB, base string, tagger tags.Reader) *Podcasts {
|
||||||
|
return &Podcasts{
|
||||||
|
db: db,
|
||||||
|
baseDir: base,
|
||||||
|
tagger: tagger,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Podcasts) GetPodcastOrAll(userID int, id int, includeEpisodes bool) ([]*db.Podcast, error) {
|
func (p *Podcasts) GetPodcastOrAll(userID int, id int, includeEpisodes bool) ([]*db.Podcast, error) {
|
||||||
podcasts := []*db.Podcast{}
|
podcasts := []*db.Podcast{}
|
||||||
q := p.DB.Where("user_id=?", userID)
|
q := p.db.Where("user_id=?", userID)
|
||||||
if id != 0 {
|
if id != 0 {
|
||||||
q = q.Where("id=?", id)
|
q = q.Where("id=?", id)
|
||||||
}
|
}
|
||||||
@@ -55,7 +64,7 @@ func (p *Podcasts) GetPodcastOrAll(userID int, id int, includeEpisodes bool) ([]
|
|||||||
|
|
||||||
func (p *Podcasts) GetPodcastEpisodes(podcastID int) ([]*db.PodcastEpisode, error) {
|
func (p *Podcasts) GetPodcastEpisodes(podcastID int) ([]*db.PodcastEpisode, error) {
|
||||||
episodes := []*db.PodcastEpisode{}
|
episodes := []*db.PodcastEpisode{}
|
||||||
err := p.DB.
|
err := p.db.
|
||||||
Where("podcast_id=?", podcastID).
|
Where("podcast_id=?", podcastID).
|
||||||
Order("publish_date DESC").
|
Order("publish_date DESC").
|
||||||
Find(&episodes).
|
Find(&episodes).
|
||||||
@@ -75,12 +84,12 @@ func (p *Podcasts) AddNewPodcast(rssURL string, feed *gofeed.Feed,
|
|||||||
Title: feed.Title,
|
Title: feed.Title,
|
||||||
URL: rssURL,
|
URL: rssURL,
|
||||||
}
|
}
|
||||||
podPath := podcast.Fullpath(p.PodcastBasePath)
|
podPath := podcast.Fullpath(p.baseDir)
|
||||||
err := os.Mkdir(podPath, 0755)
|
err := os.Mkdir(podPath, 0755)
|
||||||
if err != nil && !os.IsExist(err) {
|
if err != nil && !os.IsExist(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := p.DB.Save(&podcast).Error; err != nil {
|
if err := p.db.Save(&podcast).Error; err != nil {
|
||||||
return &podcast, err
|
return &podcast, err
|
||||||
}
|
}
|
||||||
if err := p.AddNewEpisodes(&podcast, feed.Items); err != nil {
|
if err := p.AddNewEpisodes(&podcast, feed.Items); err != nil {
|
||||||
@@ -96,7 +105,7 @@ func (p *Podcasts) AddNewPodcast(rssURL string, feed *gofeed.Feed,
|
|||||||
|
|
||||||
func (p *Podcasts) SetAutoDownload(podcastID int, setting db.PodcastAutoDownload) error {
|
func (p *Podcasts) SetAutoDownload(podcastID int, setting db.PodcastAutoDownload) error {
|
||||||
podcast := db.Podcast{}
|
podcast := db.Podcast{}
|
||||||
err := p.DB.
|
err := p.db.
|
||||||
Where("id=?", podcastID).
|
Where("id=?", podcastID).
|
||||||
First(&podcast).
|
First(&podcast).
|
||||||
Error
|
Error
|
||||||
@@ -104,7 +113,7 @@ func (p *Podcasts) SetAutoDownload(podcastID int, setting db.PodcastAutoDownload
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
podcast.AutoDownload = setting
|
podcast.AutoDownload = setting
|
||||||
if err := p.DB.Save(&podcast).Error; err != nil {
|
if err := p.db.Save(&podcast).Error; err != nil {
|
||||||
return fmt.Errorf("save setting: %w", err)
|
return fmt.Errorf("save setting: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -123,7 +132,7 @@ func getEntriesAfterDate(feed []*gofeed.Item, after time.Time) []*gofeed.Item {
|
|||||||
|
|
||||||
func (p *Podcasts) AddNewEpisodes(podcast *db.Podcast, items []*gofeed.Item) error {
|
func (p *Podcasts) AddNewEpisodes(podcast *db.Podcast, items []*gofeed.Item) error {
|
||||||
podcastEpisode := db.PodcastEpisode{}
|
podcastEpisode := db.PodcastEpisode{}
|
||||||
err := p.DB.
|
err := p.db.
|
||||||
Where("podcast_id=?", podcast.ID).
|
Where("podcast_id=?", podcast.ID).
|
||||||
Order("publish_date DESC").
|
Order("publish_date DESC").
|
||||||
First(&podcastEpisode).Error
|
First(&podcastEpisode).Error
|
||||||
@@ -192,13 +201,13 @@ func (p *Podcasts) AddEpisode(podcastID int, item *gofeed.Item) (*db.PodcastEpis
|
|||||||
}
|
}
|
||||||
|
|
||||||
if episode, ok := p.findEnclosureAudio(podcastID, duration, item); ok {
|
if episode, ok := p.findEnclosureAudio(podcastID, duration, item); ok {
|
||||||
if err := p.DB.Save(episode).Error; err != nil {
|
if err := p.db.Save(episode).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return episode, nil
|
return episode, nil
|
||||||
}
|
}
|
||||||
if episode, ok := p.findMediaAudio(podcastID, duration, item); ok {
|
if episode, ok := p.findMediaAudio(podcastID, duration, item); ok {
|
||||||
if err := p.DB.Save(episode).Error; err != nil {
|
if err := p.db.Save(episode).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return episode, nil
|
return episode, nil
|
||||||
@@ -259,7 +268,7 @@ func (p *Podcasts) findMediaAudio(podcastID, duration int,
|
|||||||
|
|
||||||
func (p *Podcasts) RefreshPodcasts() error {
|
func (p *Podcasts) RefreshPodcasts() error {
|
||||||
podcasts := []*db.Podcast{}
|
podcasts := []*db.Podcast{}
|
||||||
if err := p.DB.Find(&podcasts).Error; err != nil {
|
if err := p.db.Find(&podcasts).Error; err != nil {
|
||||||
return fmt.Errorf("find podcasts: %w", err)
|
return fmt.Errorf("find podcasts: %w", err)
|
||||||
}
|
}
|
||||||
var errs *multierr.Err
|
var errs *multierr.Err
|
||||||
@@ -271,7 +280,7 @@ func (p *Podcasts) RefreshPodcasts() error {
|
|||||||
|
|
||||||
func (p *Podcasts) RefreshPodcastsForUser(userID int) error {
|
func (p *Podcasts) RefreshPodcastsForUser(userID int) error {
|
||||||
podcasts := []*db.Podcast{}
|
podcasts := []*db.Podcast{}
|
||||||
err := p.DB.
|
err := p.db.
|
||||||
Where("user_id=?", userID).
|
Where("user_id=?", userID).
|
||||||
Find(&podcasts).
|
Find(&podcasts).
|
||||||
Error
|
Error
|
||||||
@@ -304,7 +313,7 @@ func (p *Podcasts) refreshPodcasts(podcasts []*db.Podcast) error {
|
|||||||
|
|
||||||
func (p *Podcasts) DownloadPodcastAll(podcastID int) error {
|
func (p *Podcasts) DownloadPodcastAll(podcastID int) error {
|
||||||
podcastEpisodes := []db.PodcastEpisode{}
|
podcastEpisodes := []db.PodcastEpisode{}
|
||||||
err := p.DB.
|
err := p.db.
|
||||||
Where("podcast_id=?", podcastID).
|
Where("podcast_id=?", podcastID).
|
||||||
Find(&podcastEpisodes).
|
Find(&podcastEpisodes).
|
||||||
Error
|
Error
|
||||||
@@ -322,7 +331,7 @@ func (p *Podcasts) DownloadPodcastAll(podcastID int) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("finished downloading episode: %q", episode.Title)
|
log.Printf("finished downloading episode: %q", episode.Title)
|
||||||
time.Sleep(DownloadAllWaitInterval)
|
time.Sleep(downloadAllWaitInterval)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
@@ -331,14 +340,14 @@ func (p *Podcasts) DownloadPodcastAll(podcastID int) error {
|
|||||||
func (p *Podcasts) DownloadEpisode(episodeID int) error {
|
func (p *Podcasts) DownloadEpisode(episodeID int) error {
|
||||||
podcastEpisode := db.PodcastEpisode{}
|
podcastEpisode := db.PodcastEpisode{}
|
||||||
podcast := db.Podcast{}
|
podcast := db.Podcast{}
|
||||||
err := p.DB.
|
err := p.db.
|
||||||
Where("id=?", episodeID).
|
Where("id=?", episodeID).
|
||||||
First(&podcastEpisode).
|
First(&podcastEpisode).
|
||||||
Error
|
Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get podcast episode by id: %w", err)
|
return fmt.Errorf("get podcast episode by id: %w", err)
|
||||||
}
|
}
|
||||||
err = p.DB.
|
err = p.db.
|
||||||
Where("id=?", podcastEpisode.PodcastID).
|
Where("id=?", podcastEpisode.PodcastID).
|
||||||
First(&podcast).
|
First(&podcast).
|
||||||
Error
|
Error
|
||||||
@@ -350,7 +359,7 @@ func (p *Podcasts) DownloadEpisode(episodeID int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
podcastEpisode.Status = db.PodcastEpisodeStatusDownloading
|
podcastEpisode.Status = db.PodcastEpisodeStatusDownloading
|
||||||
p.DB.Save(&podcastEpisode)
|
p.db.Save(&podcastEpisode)
|
||||||
// nolint: bodyclose
|
// nolint: bodyclose
|
||||||
resp, err := http.Get(podcastEpisode.AudioURL)
|
resp, err := http.Get(podcastEpisode.AudioURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -365,14 +374,14 @@ func (p *Podcasts) DownloadEpisode(episodeID int) error {
|
|||||||
filename = path.Base(audioURL.Path)
|
filename = path.Base(audioURL.Path)
|
||||||
}
|
}
|
||||||
filename = p.findUniqueEpisodeName(&podcast, &podcastEpisode, filename)
|
filename = p.findUniqueEpisodeName(&podcast, &podcastEpisode, filename)
|
||||||
audioFile, err := os.Create(path.Join(podcast.Fullpath(p.PodcastBasePath), filename))
|
audioFile, err := os.Create(path.Join(podcast.Fullpath(p.baseDir), filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create audio file: %w", err)
|
return fmt.Errorf("create audio file: %w", err)
|
||||||
}
|
}
|
||||||
podcastEpisode.Filename = filename
|
podcastEpisode.Filename = filename
|
||||||
sanTitle := strings.ReplaceAll(podcast.Title, "/", "_")
|
sanTitle := strings.ReplaceAll(podcast.Title, "/", "_")
|
||||||
podcastEpisode.Path = path.Join(sanTitle, filename)
|
podcastEpisode.Path = path.Join(sanTitle, filename)
|
||||||
p.DB.Save(&podcastEpisode)
|
p.db.Save(&podcastEpisode)
|
||||||
go func() {
|
go func() {
|
||||||
if err := p.doPodcastDownload(&podcastEpisode, audioFile, resp.Body); err != nil {
|
if err := p.doPodcastDownload(&podcastEpisode, audioFile, resp.Body); err != nil {
|
||||||
log.Printf("error downloading podcast: %v", err)
|
log.Printf("error downloading podcast: %v", err)
|
||||||
@@ -385,18 +394,18 @@ func (p *Podcasts) findUniqueEpisodeName(
|
|||||||
podcast *db.Podcast,
|
podcast *db.Podcast,
|
||||||
podcastEpisode *db.PodcastEpisode,
|
podcastEpisode *db.PodcastEpisode,
|
||||||
filename string) string {
|
filename string) string {
|
||||||
podcastPath := path.Join(podcast.Fullpath(p.PodcastBasePath), filename)
|
podcastPath := path.Join(podcast.Fullpath(p.baseDir), filename)
|
||||||
if _, err := os.Stat(podcastPath); os.IsNotExist(err) {
|
if _, err := os.Stat(podcastPath); os.IsNotExist(err) {
|
||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
sanitizedTitle := strings.ReplaceAll(podcastEpisode.Title, "/", "_")
|
sanitizedTitle := strings.ReplaceAll(podcastEpisode.Title, "/", "_")
|
||||||
titlePath := fmt.Sprintf("%s%s", sanitizedTitle, filepath.Ext(filename))
|
titlePath := fmt.Sprintf("%s%s", sanitizedTitle, filepath.Ext(filename))
|
||||||
podcastPath = path.Join(podcast.Fullpath(p.PodcastBasePath), titlePath)
|
podcastPath = path.Join(podcast.Fullpath(p.baseDir), titlePath)
|
||||||
if _, err := os.Stat(podcastPath); os.IsNotExist(err) {
|
if _, err := os.Stat(podcastPath); os.IsNotExist(err) {
|
||||||
return titlePath
|
return titlePath
|
||||||
}
|
}
|
||||||
// try to find a filename like FILENAME (1).mp3 incrementing
|
// try to find a filename like FILENAME (1).mp3 incrementing
|
||||||
return findEpisode(podcast.Fullpath(p.PodcastBasePath), filename, 1)
|
return findEpisode(podcast.Fullpath(p.baseDir), filename, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findEpisode(base, filename string, count int) string {
|
func findEpisode(base, filename string, count int) string {
|
||||||
@@ -442,7 +451,7 @@ func (p *Podcasts) downloadPodcastCover(podPath string, podcast *db.Podcast) err
|
|||||||
podcastPath := filepath.Clean(strings.ReplaceAll(podcast.Title, "/", "_"))
|
podcastPath := filepath.Clean(strings.ReplaceAll(podcast.Title, "/", "_"))
|
||||||
podcastFilename := fmt.Sprintf("cover%s", ext)
|
podcastFilename := fmt.Sprintf("cover%s", ext)
|
||||||
podcast.ImagePath = path.Join(podcastPath, podcastFilename)
|
podcast.ImagePath = path.Join(podcastPath, podcastFilename)
|
||||||
if err := p.DB.Save(podcast).Error; err != nil {
|
if err := p.db.Save(podcast).Error; err != nil {
|
||||||
return fmt.Errorf("save podcast: %w", err)
|
return fmt.Errorf("save podcast: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -454,24 +463,24 @@ func (p *Podcasts) doPodcastDownload(podcastEpisode *db.PodcastEpisode, file *os
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
stat, _ := file.Stat()
|
stat, _ := file.Stat()
|
||||||
podcastPath := path.Join(p.PodcastBasePath, podcastEpisode.Path)
|
podcastPath := path.Join(p.baseDir, podcastEpisode.Path)
|
||||||
podcastTags, err := tags.New(podcastPath)
|
podcastTags, err := p.tagger.Read(podcastPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error parsing podcast audio: %e", err)
|
log.Printf("error parsing podcast audio: %e", err)
|
||||||
podcastEpisode.Status = db.PodcastEpisodeStatusError
|
podcastEpisode.Status = db.PodcastEpisodeStatusError
|
||||||
p.DB.Save(podcastEpisode)
|
p.db.Save(podcastEpisode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
podcastEpisode.Bitrate = podcastTags.Bitrate()
|
podcastEpisode.Bitrate = podcastTags.Bitrate()
|
||||||
podcastEpisode.Status = db.PodcastEpisodeStatusCompleted
|
podcastEpisode.Status = db.PodcastEpisodeStatusCompleted
|
||||||
podcastEpisode.Length = podcastTags.Length()
|
podcastEpisode.Length = podcastTags.Length()
|
||||||
podcastEpisode.Size = int(stat.Size())
|
podcastEpisode.Size = int(stat.Size())
|
||||||
return p.DB.Save(podcastEpisode).Error
|
return p.db.Save(podcastEpisode).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Podcasts) DeletePodcast(userID, podcastID int) error {
|
func (p *Podcasts) DeletePodcast(userID, podcastID int) error {
|
||||||
podcast := db.Podcast{}
|
podcast := db.Podcast{}
|
||||||
err := p.DB.
|
err := p.db.
|
||||||
Where("id=? AND user_id=?", podcastID, userID).
|
Where("id=? AND user_id=?", podcastID, userID).
|
||||||
First(&podcast).
|
First(&podcast).
|
||||||
Error
|
Error
|
||||||
@@ -479,17 +488,17 @@ func (p *Podcasts) DeletePodcast(userID, podcastID int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var userCount int
|
var userCount int
|
||||||
p.DB.
|
p.db.
|
||||||
Model(&db.Podcast{}).
|
Model(&db.Podcast{}).
|
||||||
Where("title=?", podcast.Title).
|
Where("title=?", podcast.Title).
|
||||||
Count(&userCount)
|
Count(&userCount)
|
||||||
if userCount == 1 {
|
if userCount == 1 {
|
||||||
// only delete the folder if there are not multiple listeners
|
// only delete the folder if there are not multiple listeners
|
||||||
if err = os.RemoveAll(podcast.Fullpath(p.PodcastBasePath)); err != nil {
|
if err = os.RemoveAll(podcast.Fullpath(p.baseDir)); err != nil {
|
||||||
return fmt.Errorf("delete podcast directory: %w", err)
|
return fmt.Errorf("delete podcast directory: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = p.DB.
|
err = p.db.
|
||||||
Where("id=? AND user_id=?", podcastID, userID).
|
Where("id=? AND user_id=?", podcastID, userID).
|
||||||
Delete(db.Podcast{}).
|
Delete(db.Podcast{}).
|
||||||
Error
|
Error
|
||||||
@@ -501,13 +510,13 @@ func (p *Podcasts) DeletePodcast(userID, podcastID int) error {
|
|||||||
|
|
||||||
func (p *Podcasts) DeletePodcastEpisode(podcastEpisodeID int) error {
|
func (p *Podcasts) DeletePodcastEpisode(podcastEpisodeID int) error {
|
||||||
episode := db.PodcastEpisode{}
|
episode := db.PodcastEpisode{}
|
||||||
err := p.DB.First(&episode, podcastEpisodeID).Error
|
err := p.db.First(&episode, podcastEpisodeID).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
episode.Status = db.PodcastEpisodeStatusDeleted
|
episode.Status = db.PodcastEpisodeStatusDeleted
|
||||||
p.DB.Save(&episode)
|
p.db.Save(&episode)
|
||||||
if err := os.Remove(filepath.Join(p.PodcastBasePath, episode.Path)); err != nil {
|
if err := os.Remove(filepath.Join(p.baseDir, episode.Path)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -16,9 +16,9 @@ import (
|
|||||||
"github.com/karrick/godirwalk"
|
"github.com/karrick/godirwalk"
|
||||||
"github.com/rainycape/unidecode"
|
"github.com/rainycape/unidecode"
|
||||||
|
|
||||||
|
"go.senan.xyz/gonic/multierr"
|
||||||
"go.senan.xyz/gonic/server/db"
|
"go.senan.xyz/gonic/server/db"
|
||||||
"go.senan.xyz/gonic/server/mime"
|
"go.senan.xyz/gonic/server/mime"
|
||||||
"go.senan.xyz/gonic/server/scanner/stack"
|
|
||||||
"go.senan.xyz/gonic/server/scanner/tags"
|
"go.senan.xyz/gonic/server/scanner/tags"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,70 +28,347 @@ var (
|
|||||||
ErrReadingTags = errors.New("could not read tags")
|
ErrReadingTags = errors.New("could not read tags")
|
||||||
)
|
)
|
||||||
|
|
||||||
func durSince(t time.Time) time.Duration {
|
|
||||||
return time.Since(t).Truncate(10 * time.Microsecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
// decoded converts a string to it's latin equivalent.
|
|
||||||
// it will be used by the model's *UDec fields, and is only set if it
|
|
||||||
// differs from the original. the fields are used for searching.
|
|
||||||
func decoded(in string) string {
|
|
||||||
if u := unidecode.Unidecode(in); u != in {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// isScanning acts as an atomic boolean semaphore. we don't
|
|
||||||
// want to have more than one scan going on at a time
|
|
||||||
var isScanning int32 //nolint:gochecknoglobals
|
|
||||||
|
|
||||||
func IsScanning() bool {
|
|
||||||
return atomic.LoadInt32(&isScanning) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetScanning() func() {
|
|
||||||
atomic.StoreInt32(&isScanning, 1)
|
|
||||||
return func() {
|
|
||||||
atomic.StoreInt32(&isScanning, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Scanner struct {
|
type Scanner struct {
|
||||||
db *db.DB
|
db *db.DB
|
||||||
musicPath string
|
musicPaths []string
|
||||||
isFull bool
|
sorted bool
|
||||||
genreSplit string
|
genreSplit string
|
||||||
// these two are for the transaction we do for every album.
|
tagger tags.Reader
|
||||||
// the boolean is there so we dont begin or commit multiple
|
scanning *int32
|
||||||
// times in the handle album or post children callback
|
|
||||||
trTx *db.DB
|
|
||||||
trTxOpen bool
|
|
||||||
// these two are for keeping state between noted in the tree.
|
|
||||||
// eg. keep track of a parents album or the path to a cover
|
|
||||||
// we just saw that we need to commit in the post children
|
|
||||||
// callback
|
|
||||||
curAlbums *stack.Stack
|
|
||||||
curCover string
|
|
||||||
// then the rest are for stats and cleanup at the very end
|
|
||||||
seenTracks map[int]struct{} // set of p keys
|
|
||||||
seenAlbums map[int]struct{} // set of p keys
|
|
||||||
seenTracksNew int // n tracks not seen before
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(musicPath string, db *db.DB, genreSplit string) *Scanner {
|
func New(musicPaths []string, sorted bool, db *db.DB, genreSplit string, tagger tags.Reader) *Scanner {
|
||||||
return &Scanner{
|
return &Scanner{
|
||||||
db: db,
|
db: db,
|
||||||
musicPath: musicPath,
|
musicPaths: musicPaths,
|
||||||
|
sorted: sorted,
|
||||||
genreSplit: genreSplit,
|
genreSplit: genreSplit,
|
||||||
|
tagger: tagger,
|
||||||
|
scanning: new(int32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ## begin clean funcs
|
type ScanOptions struct {
|
||||||
// ## begin clean funcs
|
IsFull bool
|
||||||
// ## begin clean funcs
|
// TODO https://github.com/sentriz/gonic/issues/64
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Scanner) cleanTracks() error {
|
func (s *Scanner) IsScanning() bool {
|
||||||
|
return atomic.LoadInt32(s.scanning) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScanOptions struct {
|
||||||
|
IsFull bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) ScanAndClean(opts ScanOptions) error {
|
||||||
|
c := &collected{
|
||||||
|
seenTracks: map[int]struct{}{},
|
||||||
|
seenAlbums: map[int]struct{}{},
|
||||||
|
}
|
||||||
|
if err := s.scan(c, opts.IsFull); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.clean(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) scan(c *collected, isFull bool) error {
|
||||||
|
if s.IsScanning() {
|
||||||
|
return ErrAlreadyScanning
|
||||||
|
}
|
||||||
|
atomic.StoreInt32(s.scanning, 1)
|
||||||
|
defer atomic.StoreInt32(s.scanning, 0)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
itemErrs := multierr.Err{}
|
||||||
|
|
||||||
|
log.Println("starting scan")
|
||||||
|
defer func() {
|
||||||
|
log.Printf("finished scan in %s, +%d/%d tracks (%d err)\n",
|
||||||
|
durSince(start), c.seenTracksNew, len(c.seenTracks), itemErrs.Len())
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, musicPath := range s.musicPaths {
|
||||||
|
err := godirwalk.Walk(musicPath, &godirwalk.Options{
|
||||||
|
Callback: func(_ string, _ *godirwalk.Dirent) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
PostChildrenCallback: func(itemPath string, _ *godirwalk.Dirent) error {
|
||||||
|
return s.callback(c, isFull, musicPath, itemPath)
|
||||||
|
},
|
||||||
|
Unsorted: !s.sorted,
|
||||||
|
FollowSymbolicLinks: true,
|
||||||
|
ErrorCallback: func(path string, err error) godirwalk.ErrorAction {
|
||||||
|
itemErrs.Add(fmt.Errorf("%q: %w", path, err))
|
||||||
|
return godirwalk.SkipNode
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("walking filesystem: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.SetSetting("last_scan_time", strconv.FormatInt(time.Now().Unix(), 10)); err != nil {
|
||||||
|
return fmt.Errorf("set scan time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemErrs.Len() > 0 {
|
||||||
|
return itemErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) clean(c *collected) error {
|
||||||
|
if err := s.cleanTracks(c.seenTracks); err != nil {
|
||||||
|
return fmt.Errorf("clean tracks: %w", err)
|
||||||
|
}
|
||||||
|
if err := s.cleanAlbums(c.seenAlbums); err != nil {
|
||||||
|
return fmt.Errorf("clean albums: %w", err)
|
||||||
|
}
|
||||||
|
if err := s.cleanArtists(); err != nil {
|
||||||
|
return fmt.Errorf("clean artists: %w", err)
|
||||||
|
}
|
||||||
|
if err := s.cleanGenres(); err != nil {
|
||||||
|
return fmt.Errorf("clean genres: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) callback(c *collected, isFull bool, rootAbsPath string, itemAbsPath string) error {
|
||||||
|
if rootAbsPath == itemAbsPath {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relpath, _ := filepath.Rel(rootAbsPath, itemAbsPath)
|
||||||
|
gs, err := godirwalk.NewScanner(itemAbsPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tracks []string
|
||||||
|
var cover string
|
||||||
|
for gs.Scan() {
|
||||||
|
if isCover(gs.Name()) {
|
||||||
|
cover = gs.Name()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := mime.FromExtension(ext(gs.Name())); ok {
|
||||||
|
tracks = append(tracks, gs.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := s.db.Begin()
|
||||||
|
defer tx.Commit()
|
||||||
|
|
||||||
|
pdir, pbasename := filepath.Split(filepath.Dir(relpath))
|
||||||
|
parent := &db.Album{}
|
||||||
|
if err := tx.Where(db.Album{RootDir: rootAbsPath, LeftPath: pdir, RightPath: pbasename}).FirstOrCreate(parent).Error; err != nil {
|
||||||
|
return fmt.Errorf("first or create parent: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.seenAlbums[parent.ID] = struct{}{}
|
||||||
|
|
||||||
|
dir, basename := filepath.Split(relpath)
|
||||||
|
album := &db.Album{}
|
||||||
|
if err := tx.Where(db.Album{RootDir: rootAbsPath, LeftPath: dir, RightPath: basename}).First(album).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fmt.Errorf("find album: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := populateAlbumBasics(tx, rootAbsPath, parent, album, dir, basename, cover); err != nil {
|
||||||
|
return fmt.Errorf("populate album basics: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.seenAlbums[album.ID] = struct{}{}
|
||||||
|
|
||||||
|
sort.Strings(tracks)
|
||||||
|
for i, basename := range tracks {
|
||||||
|
abspath := filepath.Join(itemAbsPath, basename)
|
||||||
|
if err := s.populateTrackAndAlbumArtists(tx, c, i, album, basename, abspath, isFull); err != nil {
|
||||||
|
return fmt.Errorf("process %q: %w", "", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *collected, i int, album *db.Album, basename string, abspath string, isFull bool) error {
|
||||||
|
track := &db.Track{AlbumID: album.ID, Filename: filepath.Base(basename)}
|
||||||
|
if err := tx.Where(track).First(track).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fmt.Errorf("query track: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.seenTracks[track.ID] = struct{}{}
|
||||||
|
|
||||||
|
stat, err := os.Stat(abspath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stating %q: %w", basename, err)
|
||||||
|
}
|
||||||
|
if !isFull && stat.ModTime().Before(track.UpdatedAt) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
trags, err := s.tagger.Read(abspath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%v: %w", err, ErrReadingTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
artistName := trags.SomeAlbumArtist()
|
||||||
|
albumArtist, err := s.populateAlbumArtist(tx, artistName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("populate artist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := populateTrack(tx, album, albumArtist, track, trags, basename, int(stat.Size())); err != nil {
|
||||||
|
return fmt.Errorf("process %q: %w", basename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.seenTracks[track.ID] = struct{}{}
|
||||||
|
c.seenTracksNew++
|
||||||
|
|
||||||
|
genreNames := strings.Split(trags.SomeGenre(), s.genreSplit)
|
||||||
|
genreIDs, err := s.populateGenres(tx, track, genreNames)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("populate genres: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.populateTrackGenres(tx, track, genreIDs); err != nil {
|
||||||
|
return fmt.Errorf("propulate track genres: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// metadata for the album table comes only from the the first track's tags
|
||||||
|
if i > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := populateAlbum(tx, album, albumArtist, trags, stat.ModTime()); err != nil {
|
||||||
|
return fmt.Errorf("propulate album: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := populateAlbumGenres(tx, album, genreIDs); err != nil {
|
||||||
|
return fmt.Errorf("populate album genres: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateAlbum(tx *db.DB, album *db.Album, albumArtist *db.Artist, trags tags.Parser, modTime time.Time) error {
|
||||||
|
albumName := trags.SomeAlbum()
|
||||||
|
album.TagTitle = albumName
|
||||||
|
album.TagTitleUDec = decoded(albumName)
|
||||||
|
album.TagBrainzID = trags.AlbumBrainzID()
|
||||||
|
album.TagYear = trags.Year()
|
||||||
|
album.TagArtistID = albumArtist.ID
|
||||||
|
album.ModifiedAt = modTime
|
||||||
|
|
||||||
|
if err := tx.Save(&album).Error; err != nil {
|
||||||
|
return fmt.Errorf("saving album: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateAlbumBasics(tx *db.DB, rootAbsPath string, parent, album *db.Album, dir, basename string, cover string) error {
|
||||||
|
album.RootDir = rootAbsPath
|
||||||
|
album.LeftPath = dir
|
||||||
|
album.RightPath = basename
|
||||||
|
album.Cover = cover
|
||||||
|
album.RightPathUDec = decoded(basename)
|
||||||
|
album.ParentID = parent.ID
|
||||||
|
|
||||||
|
if err := tx.Save(&album).Error; err != nil {
|
||||||
|
return fmt.Errorf("saving album: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateTrack(tx *db.DB, album *db.Album, albumArtist *db.Artist, track *db.Track, trags tags.Parser, abspath string, size int) error {
|
||||||
|
basename := filepath.Base(abspath)
|
||||||
|
track.Filename = basename
|
||||||
|
track.FilenameUDec = decoded(basename)
|
||||||
|
track.Size = size
|
||||||
|
track.AlbumID = album.ID
|
||||||
|
track.ArtistID = albumArtist.ID
|
||||||
|
|
||||||
|
track.TagTitle = trags.Title()
|
||||||
|
track.TagTitleUDec = decoded(trags.Title())
|
||||||
|
track.TagTrackArtist = trags.Artist()
|
||||||
|
track.TagTrackNumber = trags.TrackNumber()
|
||||||
|
track.TagDiscNumber = trags.DiscNumber()
|
||||||
|
track.TagBrainzID = trags.BrainzID()
|
||||||
|
|
||||||
|
track.Length = trags.Length() // these two should be calculated
|
||||||
|
track.Bitrate = trags.Bitrate() // ...from the file instead of tags
|
||||||
|
|
||||||
|
if err := tx.Save(&track).Error; err != nil {
|
||||||
|
return fmt.Errorf("saving track: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) populateAlbumArtist(tx *db.DB, artistName string) (*db.Artist, error) {
|
||||||
|
var artist db.Artist
|
||||||
|
update := db.Artist{
|
||||||
|
Name: artistName,
|
||||||
|
NameUDec: decoded(artistName),
|
||||||
|
}
|
||||||
|
if err := tx.Where("name=?", artistName).Assign(update).FirstOrCreate(&artist).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("find or create artist: %w", err)
|
||||||
|
}
|
||||||
|
return &artist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) populateGenres(tx *db.DB, track *db.Track, names []string) ([]int, error) {
|
||||||
|
var filteredNames []string
|
||||||
|
for _, name := range names {
|
||||||
|
if clean := strings.TrimSpace(name); clean != "" {
|
||||||
|
filteredNames = append(filteredNames, clean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filteredNames) == 0 {
|
||||||
|
return []int{}, nil
|
||||||
|
}
|
||||||
|
var ids []int
|
||||||
|
for _, name := range filteredNames {
|
||||||
|
var genre db.Genre
|
||||||
|
if err := tx.FirstOrCreate(&genre, db.Genre{Name: name}).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("find or create genre: %w", err)
|
||||||
|
}
|
||||||
|
ids = append(ids, genre.ID)
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) populateTrackGenres(tx *db.DB, track *db.Track, genreIDs []int) error {
|
||||||
|
if err := tx.Where("track_id=?", track.ID).Delete(db.TrackGenre{}).Error; err != nil {
|
||||||
|
return fmt.Errorf("delete old track genre records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.InsertBulkLeftMany("track_genres", []string{"track_id", "genre_id"}, track.ID, genreIDs); err != nil {
|
||||||
|
return fmt.Errorf("insert bulk track genres: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateAlbumGenres(tx *db.DB, album *db.Album, genreIDs []int) error {
|
||||||
|
if err := tx.Where("album_id=?", album.ID).Delete(db.AlbumGenre{}).Error; err != nil {
|
||||||
|
return fmt.Errorf("delete old album genre records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.InsertBulkLeftMany("album_genres", []string{"album_id", "genre_id"}, album.ID, genreIDs); err != nil {
|
||||||
|
return fmt.Errorf("insert bulk album genres: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) cleanTracks(seenTracks map[int]struct{}) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
var previous []int
|
var previous []int
|
||||||
var missing []int64
|
var missing []int64
|
||||||
@@ -103,7 +380,7 @@ func (s *Scanner) cleanTracks() error {
|
|||||||
return fmt.Errorf("plucking ids: %w", err)
|
return fmt.Errorf("plucking ids: %w", err)
|
||||||
}
|
}
|
||||||
for _, prev := range previous {
|
for _, prev := range previous {
|
||||||
if _, ok := s.seenTracks[prev]; !ok {
|
if _, ok := seenTracks[prev]; !ok {
|
||||||
missing = append(missing, int64(prev))
|
missing = append(missing, int64(prev))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +394,7 @@ func (s *Scanner) cleanTracks() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scanner) cleanAlbums() error {
|
func (s *Scanner) cleanAlbums(seenAlbums map[int]struct{}) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
var previous []int
|
var previous []int
|
||||||
var missing []int64
|
var missing []int64
|
||||||
@@ -129,7 +406,7 @@ func (s *Scanner) cleanAlbums() error {
|
|||||||
return fmt.Errorf("plucking ids: %w", err)
|
return fmt.Errorf("plucking ids: %w", err)
|
||||||
}
|
}
|
||||||
for _, prev := range previous {
|
for _, prev := range previous {
|
||||||
if _, ok := s.seenAlbums[prev]; !ok {
|
if _, ok := seenAlbums[prev]; !ok {
|
||||||
missing = append(missing, int64(prev))
|
missing = append(missing, int64(prev))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,383 +453,50 @@ func (s *Scanner) cleanGenres() error {
|
|||||||
Where("album_genres.genre_id IS NULL").
|
Where("album_genres.genre_id IS NULL").
|
||||||
SubQuery()
|
SubQuery()
|
||||||
q := s.db.
|
q := s.db.
|
||||||
Where("genres.id IN ?", subTrack).
|
Where("genres.id IN ? AND genres.id IN ?", subTrack, subAlbum).
|
||||||
Or("genres.id IN ?", subAlbum).
|
|
||||||
Delete(&db.Genre{})
|
Delete(&db.Genre{})
|
||||||
log.Printf("finished clean genres in %s, %d removed", durSince(start), q.RowsAffected)
|
log.Printf("finished clean genres in %s, %d removed", durSince(start), q.RowsAffected)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ## begin entries
|
func ext(name string) string {
|
||||||
// ## begin entries
|
ext := filepath.Ext(name)
|
||||||
// ## begin entries
|
if len(ext) == 0 {
|
||||||
|
return ""
|
||||||
type ScanOptions struct {
|
}
|
||||||
IsFull bool
|
return ext[1:]
|
||||||
// TODO https://github.com/sentriz/gonic/issues/64
|
|
||||||
Path string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scanner) Start(opts ScanOptions) error {
|
func isCover(name string) bool {
|
||||||
if IsScanning() {
|
switch path := strings.ToLower(name); path {
|
||||||
return ErrAlreadyScanning
|
case
|
||||||
}
|
"cover.png", "cover.jpg", "cover.jpeg",
|
||||||
unSet := SetScanning()
|
"folder.png", "folder.jpg", "folder.jpeg",
|
||||||
defer unSet()
|
"album.png", "album.jpg", "album.jpeg",
|
||||||
|
"albumart.png", "albumart.jpg", "albumart.jpeg",
|
||||||
// reset state vars for the new scan
|
"front.png", "front.jpg", "front.jpeg":
|
||||||
s.isFull = opts.IsFull
|
return true
|
||||||
s.seenTracks = map[int]struct{}{}
|
default:
|
||||||
s.seenAlbums = map[int]struct{}{}
|
|
||||||
s.curAlbums = &stack.Stack{}
|
|
||||||
s.seenTracksNew = 0
|
|
||||||
|
|
||||||
// begin walking
|
|
||||||
log.Println("starting scan")
|
|
||||||
var errCount int
|
|
||||||
start := time.Now()
|
|
||||||
err := godirwalk.Walk(s.musicPath, &godirwalk.Options{
|
|
||||||
Callback: s.callbackItem,
|
|
||||||
PostChildrenCallback: s.callbackPost,
|
|
||||||
Unsorted: true,
|
|
||||||
FollowSymbolicLinks: true,
|
|
||||||
ErrorCallback: func(path string, err error) godirwalk.ErrorAction {
|
|
||||||
log.Printf("error processing `%s`: %v", path, err)
|
|
||||||
errCount++
|
|
||||||
return godirwalk.SkipNode
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("walking filesystem: %w", err)
|
|
||||||
}
|
|
||||||
log.Printf("finished scan in %s, +%d/%d tracks (%d err)\n",
|
|
||||||
durSince(start),
|
|
||||||
s.seenTracksNew,
|
|
||||||
len(s.seenTracks),
|
|
||||||
errCount,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := s.cleanTracks(); err != nil {
|
|
||||||
return fmt.Errorf("clean tracks: %w", err)
|
|
||||||
}
|
|
||||||
if err := s.cleanAlbums(); err != nil {
|
|
||||||
return fmt.Errorf("clean albums: %w", err)
|
|
||||||
}
|
|
||||||
if err := s.cleanArtists(); err != nil {
|
|
||||||
return fmt.Errorf("clean artists: %w", err)
|
|
||||||
}
|
|
||||||
if err := s.cleanGenres(); err != nil {
|
|
||||||
return fmt.Errorf("clean genres: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// finish up
|
|
||||||
strNow := strconv.FormatInt(time.Now().Unix(), 10)
|
|
||||||
s.db.SetSetting("last_scan_time", strNow)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// items are passed to the handle*() functions
|
|
||||||
type item struct {
|
|
||||||
fullPath string
|
|
||||||
relPath string
|
|
||||||
directory string
|
|
||||||
filename string
|
|
||||||
stat os.FileInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func isCover(filename string) bool {
|
|
||||||
filename = strings.ToLower(filename)
|
|
||||||
known := map[string]struct{}{
|
|
||||||
"cover.png": {},
|
|
||||||
"cover.jpg": {},
|
|
||||||
"cover.jpeg": {},
|
|
||||||
"folder.png": {},
|
|
||||||
"folder.jpg": {},
|
|
||||||
"folder.jpeg": {},
|
|
||||||
"album.png": {},
|
|
||||||
"album.jpg": {},
|
|
||||||
"album.jpeg": {},
|
|
||||||
"albumart.png": {},
|
|
||||||
"albumart.jpg": {},
|
|
||||||
"albumart.jpeg": {},
|
|
||||||
"front.png": {},
|
|
||||||
"front.jpg": {},
|
|
||||||
"front.jpeg": {},
|
|
||||||
}
|
|
||||||
_, ok := known[filename]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ## begin callbacks
|
|
||||||
// ## begin callbacks
|
|
||||||
// ## begin callbacks
|
|
||||||
|
|
||||||
func (s *Scanner) callbackItem(fullPath string, info *godirwalk.Dirent) error {
|
|
||||||
stat, err := os.Stat(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrStatingItem, err)
|
|
||||||
}
|
|
||||||
relPath, err := filepath.Rel(s.musicPath, fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting relative path: %w", err)
|
|
||||||
}
|
|
||||||
directory, filename := path.Split(relPath)
|
|
||||||
it := &item{
|
|
||||||
fullPath: fullPath,
|
|
||||||
relPath: relPath,
|
|
||||||
directory: directory,
|
|
||||||
filename: filename,
|
|
||||||
stat: stat,
|
|
||||||
}
|
|
||||||
isDir, err := info.IsDirOrSymlinkToDir()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stating link to dir: %w", err)
|
|
||||||
}
|
|
||||||
if isDir {
|
|
||||||
return s.handleAlbum(it)
|
|
||||||
}
|
|
||||||
if isCover(filename) {
|
|
||||||
s.curCover = filename
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ext := path.Ext(filename)
|
|
||||||
if ext == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, ok := mime.FromExtension(ext[1:]); ok {
|
|
||||||
return s.handleTrack(it)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scanner) callbackPost(fullPath string, info *godirwalk.Dirent) error {
|
|
||||||
defer func() {
|
|
||||||
s.curCover = ""
|
|
||||||
}()
|
|
||||||
if s.trTxOpen {
|
|
||||||
s.trTx.Commit()
|
|
||||||
s.trTxOpen = false
|
|
||||||
}
|
|
||||||
// begin taking the current album off the stack and add it's
|
|
||||||
// parent, cover that we found, etc.
|
|
||||||
album := s.curAlbums.Pop()
|
|
||||||
if album.Cover == s.curCover && album.ParentID != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
album.ParentID = s.curAlbums.PeekID()
|
|
||||||
album.Cover = s.curCover
|
|
||||||
if err := s.db.Save(album).Error; err != nil {
|
|
||||||
return fmt.Errorf("writing albums table: %w", err)
|
|
||||||
}
|
|
||||||
// we only log changed albums
|
|
||||||
log.Printf("processed folder `%s`\n",
|
|
||||||
path.Join(album.LeftPath, album.RightPath))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ## begin handlers
|
|
||||||
// ## begin handlers
|
|
||||||
// ## begin handlers
|
|
||||||
|
|
||||||
func (s *Scanner) itemUnchanged(statModTime, updatedInDB time.Time) bool {
|
|
||||||
if s.isFull {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return statModTime.Before(updatedInDB)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scanner) handleAlbum(it *item) error {
|
// decoded converts a string to it's latin equivalent.
|
||||||
if s.trTxOpen {
|
// it will be used by the model's *UDec fields, and is only set if it
|
||||||
// a transaction still being open when we handle an album can
|
// differs from the original. the fields are used for searching.
|
||||||
// happen if there is a album that contains /both/ tracks and
|
func decoded(in string) string {
|
||||||
// sub albums
|
if u := unidecode.Unidecode(in); u != in {
|
||||||
s.trTx.Commit()
|
return u
|
||||||
s.trTxOpen = false
|
|
||||||
}
|
}
|
||||||
album := &db.Album{}
|
return ""
|
||||||
defer func() {
|
|
||||||
// album's id will come from early return
|
|
||||||
// or save at the end
|
|
||||||
s.seenAlbums[album.ID] = struct{}{}
|
|
||||||
s.curAlbums.Push(album)
|
|
||||||
}()
|
|
||||||
err := s.db.
|
|
||||||
Where(db.Album{
|
|
||||||
LeftPath: it.directory,
|
|
||||||
RightPath: it.filename,
|
|
||||||
}).
|
|
||||||
First(album).
|
|
||||||
Error
|
|
||||||
if !gorm.IsRecordNotFoundError(err) &&
|
|
||||||
s.itemUnchanged(it.stat.ModTime(), album.UpdatedAt) {
|
|
||||||
// we found the record but it hasn't changed
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
album.LeftPath = it.directory
|
|
||||||
album.RightPath = it.filename
|
|
||||||
album.RightPathUDec = decoded(it.filename)
|
|
||||||
album.ModifiedAt = it.stat.ModTime()
|
|
||||||
if err := s.db.Save(album).Error; err != nil {
|
|
||||||
return fmt.Errorf("writing albums table: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scanner) handleTrack(it *item) error {
|
func durSince(t time.Time) time.Duration {
|
||||||
if !s.trTxOpen {
|
return time.Since(t).Truncate(10 * time.Microsecond)
|
||||||
s.trTx = s.db.Begin()
|
|
||||||
s.trTxOpen = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// init empty track and mark its ID (from lookup or save)
|
|
||||||
// for later cleanup later
|
|
||||||
var track db.Track
|
|
||||||
defer func() {
|
|
||||||
s.seenTracks[track.ID] = struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
album := s.curAlbums.Peek()
|
|
||||||
err := s.trTx.
|
|
||||||
Select("id, updated_at").
|
|
||||||
Where(db.Track{
|
|
||||||
AlbumID: album.ID,
|
|
||||||
Filename: it.filename,
|
|
||||||
}).
|
|
||||||
First(&track).
|
|
||||||
Error
|
|
||||||
if !gorm.IsRecordNotFoundError(err) &&
|
|
||||||
s.itemUnchanged(it.stat.ModTime(), track.UpdatedAt) {
|
|
||||||
// we found the record but it hasn't changed
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
trags, err := tags.New(it.fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return ErrReadingTags
|
|
||||||
}
|
|
||||||
|
|
||||||
genreIDs, err := s.populateGenres(&track, trags)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("populate genres: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create album and album artist records for first track in album
|
|
||||||
if album.TagTitle == "" {
|
|
||||||
albumArtist, err := s.populateAlbumArtist(trags)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("populate artist: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
albumName := trags.SomeAlbum()
|
|
||||||
album.TagTitle = albumName
|
|
||||||
album.TagTitleUDec = decoded(albumName)
|
|
||||||
album.TagBrainzID = trags.AlbumBrainzID()
|
|
||||||
album.TagYear = trags.Year()
|
|
||||||
album.TagArtistID = albumArtist.ID
|
|
||||||
|
|
||||||
if err := s.populateAlbumGenres(album, genreIDs); err != nil {
|
|
||||||
return fmt.Errorf("populate album genres: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
track.Filename = it.filename
|
|
||||||
track.FilenameUDec = decoded(it.filename)
|
|
||||||
track.Size = int(it.stat.Size())
|
|
||||||
track.AlbumID = album.ID
|
|
||||||
track.ArtistID = album.TagArtistID
|
|
||||||
|
|
||||||
track.TagTitle = trags.Title()
|
|
||||||
track.TagTitleUDec = decoded(trags.Title())
|
|
||||||
track.TagTrackArtist = trags.Artist()
|
|
||||||
track.TagTrackNumber = trags.TrackNumber()
|
|
||||||
track.TagDiscNumber = trags.DiscNumber()
|
|
||||||
track.TagBrainzID = trags.BrainzID()
|
|
||||||
|
|
||||||
track.Length = trags.Length() // these two should be calculated
|
|
||||||
track.Bitrate = trags.Bitrate() // ...from the file instead of tags
|
|
||||||
|
|
||||||
if err := s.trTx.Save(&track).Error; err != nil {
|
|
||||||
return fmt.Errorf("writing track table: %w", err)
|
|
||||||
}
|
|
||||||
s.seenTracksNew++
|
|
||||||
|
|
||||||
if err := s.populateTrackGenres(&track, genreIDs); err != nil {
|
|
||||||
return fmt.Errorf("populating track genres : %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scanner) populateAlbumArtist(trags *tags.Tags) (*db.Artist, error) {
|
type collected struct {
|
||||||
var artist db.Artist
|
seenTracks map[int]struct{}
|
||||||
artistName := trags.SomeAlbumArtist()
|
seenAlbums map[int]struct{}
|
||||||
err := s.trTx.
|
seenTracksNew int
|
||||||
Where("name=?", artistName).
|
|
||||||
Assign(db.Artist{
|
|
||||||
Name: artistName,
|
|
||||||
NameUDec: decoded(artistName),
|
|
||||||
}).
|
|
||||||
FirstOrCreate(&artist).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("find or create artist: %w", err)
|
|
||||||
}
|
|
||||||
return &artist, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scanner) populateGenres(track *db.Track, trags *tags.Tags) ([]int, error) {
|
|
||||||
var genreIDs []int
|
|
||||||
genreNames := strings.Split(trags.SomeGenre(), s.genreSplit)
|
|
||||||
for _, genreName := range genreNames {
|
|
||||||
genre := &db.Genre{}
|
|
||||||
q := s.trTx.FirstOrCreate(genre, db.Genre{
|
|
||||||
Name: genreName,
|
|
||||||
})
|
|
||||||
if err := q.Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
genreIDs = append(genreIDs, genre.ID)
|
|
||||||
}
|
|
||||||
return genreIDs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scanner) populateTrackGenres(track *db.Track, genreIDs []int) error {
|
|
||||||
err := s.trTx.
|
|
||||||
Where("track_id=?", track.ID).
|
|
||||||
Delete(db.TrackGenre{}).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("delete old track genre records: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.trTx.InsertBulkLeftMany(
|
|
||||||
"track_genres",
|
|
||||||
[]string{"track_id", "genre_id"},
|
|
||||||
track.ID,
|
|
||||||
genreIDs,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("insert bulk track genres: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scanner) populateAlbumGenres(album *db.Album, genreIDs []int) error {
|
|
||||||
err := s.trTx.
|
|
||||||
Where("album_id=?", album.ID).
|
|
||||||
Delete(db.AlbumGenre{}).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("delete old album genre records: %w", err)
|
|
||||||
}
|
|
||||||
err = s.trTx.InsertBulkLeftMany(
|
|
||||||
"album_genres",
|
|
||||||
[]string{"album_id", "genre_id"},
|
|
||||||
album.ID,
|
|
||||||
genreIDs,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("insert bulk album genres: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package scanner
|
package scanner_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -6,62 +6,319 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
|
||||||
"go.senan.xyz/gonic/server/db"
|
"go.senan.xyz/gonic/server/db"
|
||||||
|
"go.senan.xyz/gonic/server/mockfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testScanner *Scanner
|
|
||||||
|
|
||||||
func resetTables(db *db.DB) {
|
|
||||||
tx := db.Begin()
|
|
||||||
defer tx.Commit()
|
|
||||||
tx.Exec("delete from tracks")
|
|
||||||
tx.Exec("delete from artists")
|
|
||||||
tx.Exec("delete from albums")
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetTablesPause(db *db.DB, b *testing.B) {
|
|
||||||
b.StopTimer()
|
|
||||||
defer b.StartTimer()
|
|
||||||
resetTables(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkScanFresh(b *testing.B) {
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
resetTablesPause(testScanner.db, b)
|
|
||||||
_ = testScanner.Start(ScanOptions{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkScanIncremental(b *testing.B) {
|
|
||||||
// do a full scan and reset
|
|
||||||
_ = testScanner.Start(ScanOptions{})
|
|
||||||
b.ResetTimer()
|
|
||||||
// do the inc scans
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
_ = testScanner.Start(ScanOptions{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
db, err := db.NewMock()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error opening database: %v\n", err)
|
|
||||||
}
|
|
||||||
// benchmarks aren't real code are they? >:)
|
|
||||||
// here is an absolute path to my music directory
|
|
||||||
testScanner = New("/home/senan/music", db, "\n")
|
|
||||||
log.SetOutput(ioutil.Discard)
|
log.SetOutput(ioutil.Discard)
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
// RESULTS fresh
|
func TestTableCounts(t *testing.T) {
|
||||||
// 20 times / 1.436
|
t.Parallel()
|
||||||
// 20 times / 1.39
|
is := is.NewRelaxed(t)
|
||||||
|
m := mockfs.New(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
// RESULTS inc
|
m.AddItems()
|
||||||
// 100 times / 1.86
|
m.ScanAndClean()
|
||||||
// 100 times / 1.9
|
|
||||||
// 100 times / 1.5
|
var tracks int
|
||||||
// 100 times / 1.48
|
is.NoErr(m.DB().Model(&db.Track{}).Count(&tracks).Error) // not all tracks
|
||||||
|
is.Equal(tracks, 3*3*3) // not all tracks
|
||||||
|
|
||||||
|
var albums int
|
||||||
|
is.NoErr(m.DB().Model(&db.Album{}).Count(&albums).Error) // not all albums
|
||||||
|
is.Equal(albums, 13) // not all albums
|
||||||
|
|
||||||
|
var artists int
|
||||||
|
is.NoErr(m.DB().Model(&db.Artist{}).Count(&artists).Error) // not all artists
|
||||||
|
is.Equal(artists, 3) // not all artists
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParentID(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
is := is.New(t)
|
||||||
|
m := mockfs.New(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
m.AddItems()
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
var nullParentAlbums []*db.Album
|
||||||
|
is.NoErr(m.DB().Where("parent_id IS NULL").Find(&nullParentAlbums).Error) // one parent_id=NULL which is root folder
|
||||||
|
is.Equal(len(nullParentAlbums), 1) // one parent_id=NULL which is root folder
|
||||||
|
is.Equal(nullParentAlbums[0].LeftPath, "")
|
||||||
|
is.Equal(nullParentAlbums[0].RightPath, ".")
|
||||||
|
|
||||||
|
is.Equal(m.DB().Where("id=parent_id").Find(&db.Album{}).Error, gorm.ErrRecordNotFound) // no self-referencing albums
|
||||||
|
|
||||||
|
var album db.Album
|
||||||
|
var parent db.Album
|
||||||
|
is.NoErr(m.DB().Find(&album, "left_path=? AND right_path=?", "artist-0/", "album-0").Error) // album has parent ID
|
||||||
|
is.NoErr(m.DB().Find(&parent, "right_path=?", "artist-0").Error) // album has parent ID
|
||||||
|
is.Equal(album.ParentID, parent.ID) // album has parent ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdatedCover(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
is := is.NewRelaxed(t)
|
||||||
|
m := mockfs.New(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
m.AddItems()
|
||||||
|
m.ScanAndClean()
|
||||||
|
m.AddCover("artist-0/album-0/cover.jpg")
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
var album db.Album
|
||||||
|
is.NoErr(m.DB().Where("left_path=? AND right_path=?", "artist-0/", "album-0").Find(&album).Error) // album has cover
|
||||||
|
is.Equal(album.Cover, "cover.jpg") // album has cover
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoverBeforeTracks(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
is := is.New(t)
|
||||||
|
m := mockfs.New(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
m.AddCover("artist-2/album-2/cover.jpg")
|
||||||
|
m.ScanAndClean()
|
||||||
|
m.AddItems()
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
var album db.Album
|
||||||
|
is.NoErr(m.DB().Preload("TagArtist").Where("left_path=? AND right_path=?", "artist-2/", "album-2").Find(&album).Error) // album has cover
|
||||||
|
is.Equal(album.Cover, "cover.jpg") // album has cover
|
||||||
|
is.Equal(album.TagArtist.Name, "artist-2") // album artist
|
||||||
|
|
||||||
|
var tracks []*db.Track
|
||||||
|
is.NoErr(m.DB().Where("album_id=?", album.ID).Find(&tracks).Error) // album has tracks
|
||||||
|
is.Equal(len(tracks), 3) // album has tracks
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdatedTags(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
is := is.New(t)
|
||||||
|
m := mockfs.New(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
m.AddTrack("artist-10/album-10/track-10.flac")
|
||||||
|
m.SetTags("artist-10/album-10/track-10.flac", func(tags *mockfs.Tags) {
|
||||||
|
tags.RawArtist = "artist"
|
||||||
|
tags.RawAlbumArtist = "album-artist"
|
||||||
|
tags.RawAlbum = "album"
|
||||||
|
tags.RawTitle = "title"
|
||||||
|
})
|
||||||
|
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
var track db.Track
|
||||||
|
is.NoErr(m.DB().Preload("Album").Preload("Artist").Where("filename=?", "track-10.flac").Find(&track).Error) // track has tags
|
||||||
|
is.Equal(track.TagTrackArtist, "artist") // track has tags
|
||||||
|
is.Equal(track.Artist.Name, "album-artist") // track has tags
|
||||||
|
is.Equal(track.Album.TagTitle, "album") // track has tags
|
||||||
|
is.Equal(track.TagTitle, "title") // track has tags
|
||||||
|
|
||||||
|
m.SetTags("artist-10/album-10/track-10.flac", func(tags *mockfs.Tags) {
|
||||||
|
tags.RawArtist = "artist-upd"
|
||||||
|
tags.RawAlbumArtist = "album-artist-upd"
|
||||||
|
tags.RawAlbum = "album-upd"
|
||||||
|
tags.RawTitle = "title-upd"
|
||||||
|
})
|
||||||
|
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
var updated db.Track
|
||||||
|
is.NoErr(m.DB().Preload("Album").Preload("Artist").Where("filename=?", "track-10.flac").Find(&updated).Error) // updated has tags
|
||||||
|
is.Equal(updated.ID, track.ID) // updated has tags
|
||||||
|
is.Equal(updated.TagTrackArtist, "artist-upd") // updated has tags
|
||||||
|
is.Equal(updated.Artist.Name, "album-artist-upd") // updated has tags
|
||||||
|
is.Equal(updated.Album.TagTitle, "album-upd") // updated has tags
|
||||||
|
is.Equal(updated.TagTitle, "title-upd") // updated has tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
is := is.NewRelaxed(t)
|
||||||
|
m := mockfs.New(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
m.AddItems()
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
var album db.Album
|
||||||
|
is.NoErr(m.DB().Where("left_path=? AND right_path=?", "artist-2/", "album-2").Find(&album).Error) // album exists
|
||||||
|
|
||||||
|
m.RemoveAll("artist-2/album-2")
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
is.Equal(m.DB().Where("left_path=? AND right_path=?", "artist-2/", "album-2").Find(&album).Error, gorm.ErrRecordNotFound) // album doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenres(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
is := is.New(t)
|
||||||
|
m := mockfs.New(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
albumGenre := func(artist, album, genre string) error {
|
||||||
|
return m.DB().
|
||||||
|
Where("albums.left_path=? AND albums.right_path=? AND genres.name=?", artist, album, genre).
|
||||||
|
Joins("JOIN albums ON albums.id=album_genres.album_id").
|
||||||
|
Joins("JOIN genres ON genres.id=album_genres.genre_id").
|
||||||
|
Find(&db.AlbumGenre{}).
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
isAlbumGenre := func(artist, album, genreName string) {
|
||||||
|
is.Helper()
|
||||||
|
is.NoErr(albumGenre(artist, album, genreName))
|
||||||
|
}
|
||||||
|
isAlbumGenreMissing := func(artist, album, genreName string) {
|
||||||
|
is.Helper()
|
||||||
|
is.Equal(albumGenre(artist, album, genreName), gorm.ErrRecordNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
trackGenre := func(artist, album, filename, genreName string) error {
|
||||||
|
return m.DB().
|
||||||
|
Where("albums.left_path=? AND albums.right_path=? AND tracks.filename=? AND genres.name=?", artist, album, filename, genreName).
|
||||||
|
Joins("JOIN tracks ON tracks.id=track_genres.track_id").
|
||||||
|
Joins("JOIN genres ON genres.id=track_genres.genre_id").
|
||||||
|
Joins("JOIN albums ON albums.id=tracks.album_id").
|
||||||
|
Find(&db.TrackGenre{}).
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
isTrackGenre := func(artist, album, filename, genreName string) {
|
||||||
|
is.Helper()
|
||||||
|
is.NoErr(trackGenre(artist, album, filename, genreName))
|
||||||
|
}
|
||||||
|
isTrackGenreMissing := func(artist, album, filename, genreName string) {
|
||||||
|
is.Helper()
|
||||||
|
is.Equal(trackGenre(artist, album, filename, genreName), gorm.ErrRecordNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
genre := func(genre string) error {
|
||||||
|
return m.DB().Where("name=?", genre).Find(&db.Genre{}).Error
|
||||||
|
}
|
||||||
|
isGenre := func(genreName string) {
|
||||||
|
is.Helper()
|
||||||
|
is.NoErr(genre(genreName))
|
||||||
|
}
|
||||||
|
isGenreMissing := func(genreName string) {
|
||||||
|
is.Helper()
|
||||||
|
is.Equal(genre(genreName), gorm.ErrRecordNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.AddItems()
|
||||||
|
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) { tags.RawGenre = "genre-a;genre-b" })
|
||||||
|
m.SetTags("artist-0/album-0/track-1.flac", func(tags *mockfs.Tags) { tags.RawGenre = "genre-c;genre-d" })
|
||||||
|
m.SetTags("artist-1/album-2/track-0.flac", func(tags *mockfs.Tags) { tags.RawGenre = "genre-e;genre-f" })
|
||||||
|
m.SetTags("artist-1/album-2/track-1.flac", func(tags *mockfs.Tags) { tags.RawGenre = "genre-g;genre-h" })
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
isGenre("genre-a") // genre exists
|
||||||
|
isGenre("genre-b") // genre exists
|
||||||
|
isGenre("genre-c") // genre exists
|
||||||
|
isGenre("genre-d") // genre exists
|
||||||
|
|
||||||
|
isTrackGenre("artist-0/", "album-0", "track-0.flac", "genre-a") // track genre exists
|
||||||
|
isTrackGenre("artist-0/", "album-0", "track-0.flac", "genre-b") // track genre exists
|
||||||
|
isTrackGenre("artist-0/", "album-0", "track-1.flac", "genre-c") // track genre exists
|
||||||
|
isTrackGenre("artist-0/", "album-0", "track-1.flac", "genre-d") // track genre exists
|
||||||
|
isTrackGenre("artist-1/", "album-2", "track-0.flac", "genre-e") // track genre exists
|
||||||
|
isTrackGenre("artist-1/", "album-2", "track-0.flac", "genre-f") // track genre exists
|
||||||
|
isTrackGenre("artist-1/", "album-2", "track-1.flac", "genre-g") // track genre exists
|
||||||
|
isTrackGenre("artist-1/", "album-2", "track-1.flac", "genre-h") // track genre exists
|
||||||
|
|
||||||
|
isAlbumGenre("artist-0/", "album-0", "genre-a") // album genre exists
|
||||||
|
isAlbumGenre("artist-0/", "album-0", "genre-b") // album genre exists
|
||||||
|
|
||||||
|
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) { tags.RawGenre = "genre-aa;genre-bb" })
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
isTrackGenre("artist-0/", "album-0", "track-0.flac", "genre-aa") // updated track genre exists
|
||||||
|
isTrackGenre("artist-0/", "album-0", "track-0.flac", "genre-bb") // updated track genre exists
|
||||||
|
isTrackGenreMissing("artist-0/", "album-0", "track-0.flac", "genre-a") // old track genre missing
|
||||||
|
isTrackGenreMissing("artist-0/", "album-0", "track-0.flac", "genre-b") // old track genre missing
|
||||||
|
|
||||||
|
isAlbumGenreMissing("artist-0/", "album-0", "genre-a") // old album genre missing
|
||||||
|
isAlbumGenreMissing("artist-0/", "album-0", "genre-b") // old album genre missing
|
||||||
|
|
||||||
|
isGenreMissing("genre-a") // old genre missing
|
||||||
|
isGenreMissing("genre-b") // old genre missing
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiFolders(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
is := is.New(t)
|
||||||
|
m := mockfs.NewWithDirs(t, []string{"m-1", "m-2", "m-3"})
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
m.AddItemsPrefix("m-1")
|
||||||
|
m.AddItemsPrefix("m-2")
|
||||||
|
m.AddItemsPrefix("m-3")
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
var rootDirs []*db.Album
|
||||||
|
is.NoErr(m.DB().Where("parent_id IS NULL").Find(&rootDirs).Error)
|
||||||
|
is.Equal(len(rootDirs), 3)
|
||||||
|
for i, r := range rootDirs {
|
||||||
|
is.Equal(r.RootDir, filepath.Join(m.TmpDir(), fmt.Sprintf("m-%d", i+1)))
|
||||||
|
is.Equal(r.ParentID, 0)
|
||||||
|
is.Equal(r.LeftPath, "")
|
||||||
|
is.Equal(r.RightPath, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.AddCover("m-3/artist-0/album-0/cover.jpg")
|
||||||
|
m.ScanAndClean()
|
||||||
|
m.LogItems()
|
||||||
|
|
||||||
|
checkCover := func(root string, q string) {
|
||||||
|
is.Helper()
|
||||||
|
is.NoErr(m.DB().Where(q, filepath.Join(m.TmpDir(), root)).Find(&db.Album{}).Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCover("m-1", "root_dir=? AND cover IS NULL") // mf 1 no cover
|
||||||
|
checkCover("m-2", "root_dir=? AND cover IS NULL") // mf 2 no cover
|
||||||
|
checkCover("m-3", "root_dir=? AND cover='cover.jpg'") // mf 3 has cover
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAlbumForExistingArtist(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
is := is.New(t)
|
||||||
|
m := mockfs.New(t)
|
||||||
|
defer m.CleanUp()
|
||||||
|
|
||||||
|
m.AddItems()
|
||||||
|
m.ScanAndClean()
|
||||||
|
|
||||||
|
m.LogAlbums()
|
||||||
|
m.LogArtists()
|
||||||
|
|
||||||
|
var artist db.Artist
|
||||||
|
is.NoErr(m.DB().Where("name=?", "artist-2").Find(&artist).Error) // find orig artist
|
||||||
|
is.True(artist.ID > 0)
|
||||||
|
|
||||||
|
for tr := 0; tr < 3; tr++ {
|
||||||
|
m.AddTrack(fmt.Sprintf("artist-2/new-album/track-%d.mp3", tr))
|
||||||
|
m.SetTags(fmt.Sprintf("artist-2/new-album/track-%d.mp3", tr), func(tags *mockfs.Tags) {
|
||||||
|
tags.RawArtist = "artist-2"
|
||||||
|
tags.RawAlbumArtist = "artist-2"
|
||||||
|
tags.RawAlbum = "new-album"
|
||||||
|
tags.RawTitle = fmt.Sprintf("title-%d", tr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var updated db.Artist
|
||||||
|
is.NoErr(m.DB().Where("name=?", "artist-2").Find(&updated).Error) // find updated artist
|
||||||
|
is.Equal(artist.ID, updated.ID) // find updated artist
|
||||||
|
|
||||||
|
var all []*db.Artist
|
||||||
|
is.NoErr(m.DB().Find(&all).Error) // still only 3?
|
||||||
|
is.Equal(len(all), 3) // still only 3?
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
package stack
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.senan.xyz/gonic/server/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
type item struct {
|
|
||||||
value *db.Album
|
|
||||||
next *item
|
|
||||||
}
|
|
||||||
|
|
||||||
type Stack struct {
|
|
||||||
top *item
|
|
||||||
len uint
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stack) Push(v *db.Album) {
|
|
||||||
s.top = &item{
|
|
||||||
value: v,
|
|
||||||
next: s.top,
|
|
||||||
}
|
|
||||||
s.len++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stack) Pop() *db.Album {
|
|
||||||
if s.len == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
v := s.top.value
|
|
||||||
s.top = s.top.next
|
|
||||||
s.len--
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stack) Peek() *db.Album {
|
|
||||||
if s.len == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.top.value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stack) PeekID() int {
|
|
||||||
if s.len == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return s.top.value.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stack) String() string {
|
|
||||||
var str strings.Builder
|
|
||||||
str.WriteString("[")
|
|
||||||
for i, f := uint(0), s.top; i < s.len; i++ {
|
|
||||||
str.WriteString(fmt.Sprintf("%d, ", f.value.ID))
|
|
||||||
f = f.next
|
|
||||||
}
|
|
||||||
str.WriteString("]")
|
|
||||||
return str.String()
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package stack
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.senan.xyz/gonic/server/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFolderStack(t *testing.T) {
|
|
||||||
sta := &Stack{}
|
|
||||||
sta.Push(&db.Album{ID: 3})
|
|
||||||
sta.Push(&db.Album{ID: 4})
|
|
||||||
sta.Push(&db.Album{ID: 5})
|
|
||||||
sta.Push(&db.Album{ID: 6})
|
|
||||||
expected := "[6, 5, 4, 3, ]"
|
|
||||||
actual := sta.String()
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("first stack: expected string "+
|
|
||||||
"%q, got %q", expected, actual)
|
|
||||||
}
|
|
||||||
//
|
|
||||||
sta = &Stack{}
|
|
||||||
sta.Push(&db.Album{ID: 27})
|
|
||||||
sta.Push(&db.Album{ID: 4})
|
|
||||||
sta.Peek()
|
|
||||||
sta.Push(&db.Album{ID: 5})
|
|
||||||
sta.Push(&db.Album{ID: 6})
|
|
||||||
sta.Push(&db.Album{ID: 7})
|
|
||||||
sta.Pop()
|
|
||||||
expected = "[6, 5, 4, 27, ]"
|
|
||||||
actual = sta.String()
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("second stack: expected string "+
|
|
||||||
"%q, got %q", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,56 @@ import (
|
|||||||
"github.com/nicksellen/audiotags"
|
"github.com/nicksellen/audiotags"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TagReader struct{}
|
||||||
|
|
||||||
|
func (*TagReader) Read(abspath string) (Parser, error) {
|
||||||
|
raw, props, err := audiotags.Read(abspath)
|
||||||
|
return &Tagger{raw, props}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tagger struct {
|
||||||
|
raw map[string]string
|
||||||
|
props *audiotags.AudioProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tagger) first(keys ...string) string {
|
||||||
|
for _, key := range keys {
|
||||||
|
if val, ok := t.raw[key]; ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tagger) Title() string { return t.first("title") }
|
||||||
|
func (t *Tagger) BrainzID() string { return t.first("musicbrainz_trackid") }
|
||||||
|
func (t *Tagger) Artist() string { return t.first("artist") }
|
||||||
|
func (t *Tagger) Album() string { return t.first("album") }
|
||||||
|
func (t *Tagger) AlbumArtist() string { return t.first("albumartist", "album artist") }
|
||||||
|
func (t *Tagger) AlbumBrainzID() string { return t.first("musicbrainz_albumid") }
|
||||||
|
func (t *Tagger) Genre() string { return t.first("genre") }
|
||||||
|
func (t *Tagger) TrackNumber() int { return intSep(t.first("tracknumber"), "/") } // eg. 5/12
|
||||||
|
func (t *Tagger) DiscNumber() int { return intSep(t.first("discnumber"), "/") } // eg. 1/2
|
||||||
|
func (t *Tagger) Length() int { return t.props.Length }
|
||||||
|
func (t *Tagger) Bitrate() int { return t.props.Bitrate }
|
||||||
|
func (t *Tagger) Year() int { return intSep(t.first("originaldate", "date", "year"), "-") }
|
||||||
|
|
||||||
|
func (t *Tagger) SomeAlbum() string { return first("Unknown Album", t.Album()) }
|
||||||
|
func (t *Tagger) SomeArtist() string { return first("Unknown Artist", t.Artist()) }
|
||||||
|
func (t *Tagger) SomeAlbumArtist() string {
|
||||||
|
return first("Unknown Artist", t.AlbumArtist(), t.Artist())
|
||||||
|
}
|
||||||
|
func (t *Tagger) SomeGenre() string { return first("Unknown Genre", t.Genre()) }
|
||||||
|
|
||||||
|
func first(or string, strs ...string) string {
|
||||||
|
for _, str := range strs {
|
||||||
|
if str != "" {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return or
|
||||||
|
}
|
||||||
|
|
||||||
func intSep(in, sep string) int {
|
func intSep(in, sep string) int {
|
||||||
if in == "" {
|
if in == "" {
|
||||||
return 0
|
return 0
|
||||||
@@ -19,48 +69,26 @@ func intSep(in, sep string) int {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tags struct {
|
type Reader interface {
|
||||||
raw map[string]string
|
Read(abspath string) (Parser, error)
|
||||||
props *audiotags.AudioProperties
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(path string) (*Tags, error) {
|
type Parser interface {
|
||||||
raw, props, err := audiotags.Read(path)
|
Title() string
|
||||||
return &Tags{raw, props}, err
|
BrainzID() string
|
||||||
}
|
Artist() string
|
||||||
|
Album() string
|
||||||
func (t *Tags) firstTag(keys ...string) string {
|
AlbumArtist() string
|
||||||
for _, key := range keys {
|
AlbumBrainzID() string
|
||||||
if val, ok := t.raw[key]; ok {
|
Genre() string
|
||||||
return val
|
TrackNumber() int
|
||||||
}
|
DiscNumber() int
|
||||||
}
|
Length() int
|
||||||
return ""
|
Bitrate() int
|
||||||
}
|
Year() int
|
||||||
|
|
||||||
func (t *Tags) Title() string { return t.firstTag("title") }
|
SomeAlbum() string
|
||||||
func (t *Tags) BrainzID() string { return t.firstTag("musicbrainz_trackid") }
|
SomeArtist() string
|
||||||
func (t *Tags) Artist() string { return t.firstTag("artist") }
|
SomeAlbumArtist() string
|
||||||
func (t *Tags) Album() string { return t.firstTag("album") }
|
SomeGenre() string
|
||||||
func (t *Tags) AlbumArtist() string { return t.firstTag("albumartist", "album artist") }
|
|
||||||
func (t *Tags) AlbumBrainzID() string { return t.firstTag("musicbrainz_albumid") }
|
|
||||||
func (t *Tags) Genre() string { return t.firstTag("genre") }
|
|
||||||
func (t *Tags) TrackNumber() int { return intSep(t.firstTag("tracknumber"), "/") } // eg. 5/12
|
|
||||||
func (t *Tags) DiscNumber() int { return intSep(t.firstTag("discnumber"), "/") } // eg. 1/2
|
|
||||||
func (t *Tags) Length() int { return t.props.Length }
|
|
||||||
func (t *Tags) Bitrate() int { return t.props.Bitrate }
|
|
||||||
func (t *Tags) Year() int { return intSep(t.firstTag("originaldate", "date", "year"), "-") }
|
|
||||||
|
|
||||||
func (t *Tags) SomeAlbum() string { return first("Unknown Album", t.Album()) }
|
|
||||||
func (t *Tags) SomeArtist() string { return first("Unknown Artist", t.Artist()) }
|
|
||||||
func (t *Tags) SomeAlbumArtist() string { return first("Unknown Artist", t.AlbumArtist(), t.Artist()) }
|
|
||||||
func (t *Tags) SomeGenre() string { return first("Unknown Genre", t.Genre()) }
|
|
||||||
|
|
||||||
func first(or string, strs ...string) string {
|
|
||||||
for _, str := range strs {
|
|
||||||
if str != "" {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return or
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,8 +146,15 @@ func (s *Scrobbler) Scrobble(user *db.User, track *db.Track, stamp time.Time, su
|
|||||||
if user.LastFMSession == "" {
|
if user.LastFMSession == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
apiKey := s.DB.GetSetting("lastfm_api_key")
|
apiKey, err := s.DB.GetSetting("lastfm_api_key")
|
||||||
secret := s.DB.GetSetting("lastfm_secret")
|
if err != nil {
|
||||||
|
return fmt.Errorf("get api key: %w", err)
|
||||||
|
}
|
||||||
|
secret, err := s.DB.GetSetting("lastfm_secret")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// fetch user to get lastfm session
|
// fetch user to get lastfm session
|
||||||
if user.LastFMSession == "" {
|
if user.LastFMSession == "" {
|
||||||
return fmt.Errorf("you don't have a last.fm session: %w", ErrLastFM)
|
return fmt.Errorf("you don't have a last.fm session: %w", ErrLastFM)
|
||||||
@@ -169,7 +176,7 @@ func (s *Scrobbler) Scrobble(user *db.User, track *db.Track, stamp time.Time, su
|
|||||||
params.Add("mbid", track.TagBrainzID)
|
params.Add("mbid", track.TagBrainzID)
|
||||||
params.Add("albumArtist", track.Artist.Name)
|
params.Add("albumArtist", track.Artist.Name)
|
||||||
params.Add("api_sig", getParamSignature(params, secret))
|
params.Add("api_sig", getParamSignature(params, secret))
|
||||||
_, err := makeRequest("POST", params)
|
_, err = makeRequest("POST", params)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
"github.com/wader/gormstore"
|
"github.com/wader/gormstore"
|
||||||
|
|
||||||
"go.senan.xyz/gonic/server/assets"
|
"go.senan.xyz/gonic/server/assets"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
"go.senan.xyz/gonic/server/jukebox"
|
"go.senan.xyz/gonic/server/jukebox"
|
||||||
"go.senan.xyz/gonic/server/podcasts"
|
"go.senan.xyz/gonic/server/podcasts"
|
||||||
"go.senan.xyz/gonic/server/scanner"
|
"go.senan.xyz/gonic/server/scanner"
|
||||||
|
"go.senan.xyz/gonic/server/scanner/tags"
|
||||||
"go.senan.xyz/gonic/server/scrobble"
|
"go.senan.xyz/gonic/server/scrobble"
|
||||||
"go.senan.xyz/gonic/server/scrobble/lastfm"
|
"go.senan.xyz/gonic/server/scrobble/lastfm"
|
||||||
"go.senan.xyz/gonic/server/scrobble/listenbrainz"
|
"go.senan.xyz/gonic/server/scrobble/listenbrainz"
|
||||||
@@ -48,7 +50,9 @@ func New(opts Options) (*Server, error) {
|
|||||||
opts.CachePath = filepath.Clean(opts.CachePath)
|
opts.CachePath = filepath.Clean(opts.CachePath)
|
||||||
opts.PodcastPath = filepath.Clean(opts.PodcastPath)
|
opts.PodcastPath = filepath.Clean(opts.PodcastPath)
|
||||||
|
|
||||||
scanner := scanner.New(opts.MusicPath, opts.DB, opts.GenreSplit)
|
tagger := &tags.TagReader{}
|
||||||
|
|
||||||
|
scanner := scanner.New(opts.MusicPaths, false, opts.DB, opts.GenreSplit, tagger)
|
||||||
base := &ctrlbase.Controller{
|
base := &ctrlbase.Controller{
|
||||||
DB: opts.DB,
|
DB: opts.DB,
|
||||||
MusicPath: opts.MusicPath,
|
MusicPath: opts.MusicPath,
|
||||||
@@ -63,12 +67,21 @@ func New(opts Options) (*Server, error) {
|
|||||||
}
|
}
|
||||||
r.Use(base.WithCORS)
|
r.Use(base.WithCORS)
|
||||||
|
|
||||||
sessKey := opts.DB.GetOrCreateKey("session_key")
|
sessKey, err := opts.DB.GetSetting("session_key")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get session key: %w", err)
|
||||||
|
}
|
||||||
|
if sessKey == "" {
|
||||||
|
if err := opts.DB.SetSetting("session_key", string(securecookie.GenerateRandomKey(32))); err != nil {
|
||||||
|
return nil, fmt.Errorf("set session key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sessDB := gormstore.New(opts.DB.DB, []byte(sessKey))
|
sessDB := gormstore.New(opts.DB.DB, []byte(sessKey))
|
||||||
sessDB.SessionOpts.HttpOnly = true
|
sessDB.SessionOpts.HttpOnly = true
|
||||||
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
|
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
|
||||||
|
|
||||||
podcast := &podcasts.Podcasts{DB: opts.DB, PodcastBasePath: opts.PodcastPath}
|
podcast := podcasts.New(opts.DB, opts.PodcastPath, tagger)
|
||||||
|
|
||||||
ctrlAdmin, err := ctrladmin.New(base, sessDB, podcast)
|
ctrlAdmin, err := ctrladmin.New(base, sessDB, podcast)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,11 +91,10 @@ func New(opts Options) (*Server, error) {
|
|||||||
Controller: base,
|
Controller: base,
|
||||||
CachePath: opts.CachePath,
|
CachePath: opts.CachePath,
|
||||||
CoverCachePath: opts.CoverCachePath,
|
CoverCachePath: opts.CoverCachePath,
|
||||||
Scrobblers: []scrobble.Scrobbler{
|
PodcastsPath: opts.PodcastPath,
|
||||||
&lastfm.Scrobbler{DB: opts.DB},
|
Jukebox: &jukebox.Jukebox{},
|
||||||
&listenbrainz.Scrobbler{},
|
Scrobblers: []scrobble.Scrobbler{&lastfm.Scrobbler{DB: opts.DB}, &listenbrainz.Scrobbler{}},
|
||||||
},
|
Podcasts: podcast,
|
||||||
Podcasts: podcast,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupMisc(r, base)
|
setupMisc(r, base)
|
||||||
@@ -272,7 +284,7 @@ func (s *Server) StartScanTicker(dur time.Duration) (FuncExecute, FuncInterrupt)
|
|||||||
return nil
|
return nil
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
go func() {
|
go func() {
|
||||||
if err := s.scanner.Start(scanner.ScanOptions{}); err != nil {
|
if err := s.scanner.ScanAndClean(scanner.ScanOptions{}); err != nil {
|
||||||
log.Printf("error scanning: %v", err)
|
log.Printf("error scanning: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
Reference in New Issue
Block a user