diff --git a/.github/workflows/nightly-release.yaml b/.github/workflows/nightly-release.yaml index 73a8b29..67ee93d 100644 --- a/.github/workflows/nightly-release.yaml +++ b/.github/workflows/nightly-release.yaml @@ -1,7 +1,7 @@ name: Nightly Release on: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" workflow_dispatch: {} jobs: test: @@ -21,7 +21,7 @@ jobs: - name: Lint uses: golangci/golangci-lint-action@v2 with: - version: v1.40.0 + version: v1.42.1 skip-go-installation: true - name: Test run: go test ./... diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 52d5b9d..649edff 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,7 +21,7 @@ jobs: - name: Lint uses: golangci/golangci-lint-action@v2 with: - version: v1.40.0 + version: v1.42.1 skip-go-installation: true - name: Test run: go test ./... diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 86ee535..38f1837 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,7 +22,7 @@ jobs: - name: Lint uses: golangci/golangci-lint-action@v2 with: - version: v1.40.0 + version: v1.42.1 skip-go-installation: true - name: Test run: go test ./... diff --git a/.golangci.yml b/.golangci.yml index 1e76e5d..0259f85 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,57 +1,51 @@ run: skip-dirs: - - server/assets + - server/assets skip-dirs-use-default: true linters: disable-all: true enable: - - bodyclose - - deadcode - - depguard - - dogsled - - errcheck - - exportloopref - - gochecknoglobals - - gochecknoinits - - goconst - - gocritic - - gocyclo - - goerr113 - - golint - - goprintffuncname - - gosec - - gosimple - - govet - - ineffassign - - lll - - misspell - - nakedret - - rowserrcheck - - staticcheck - - structcheck - - stylecheck - - typecheck - - unconvert - - varcheck + - bodyclose + - deadcode + - depguard + - dogsled + - errcheck + - exportloopref + - gochecknoglobals + - gochecknoinits + - goconst + - gocritic + - gocyclo + - goerr113 + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - revive + - rowserrcheck + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - varcheck issues: exclude-rules: - - path: _test\.go - linters: - - errcheck - - gochecknoglobals - - text: "weak cryptographic primitive" - linters: - - gosec - - text: "weak random number generator" - linters: - - gosec - - # TODO: fix these - - text: "should have comment" - linters: - - golint - - text: "at least one file in a package should have a package comment" - linters: - - stylecheck + - path: _test\.go + linters: + - errcheck + - gochecknoglobals + - text: "weak cryptographic primitive" + linters: + - gosec + - text: "weak random number generator" + linters: + - gosec + - text: "at least one file in a package should have a package comment" + linters: + - stylecheck diff --git a/cmd/gonic/gonic.go b/cmd/gonic/gonic.go index 3cb9116..dafdeae 100644 --- a/cmd/gonic/gonic.go +++ b/cmd/gonic/gonic.go @@ -86,7 +86,7 @@ func main() { } } - db, err := db.New(*confDBPath) + dbc, err := db.New(*confDBPath, db.DefaultOptions()) if err != nil { log.Fatalf("error opening database: %v\n", err) } @@ -106,8 +106,7 @@ func main() { JukeboxEnabled: *confJukeboxEnabled, }) if err != nil { - log.Printf("error creating server: %v\n", err) - return + log.Panicf("error creating server: %v\n", err) } var g run.Group @@ -123,6 +122,6 @@ func main() { } if err := g.Run(); err != nil { - log.Printf("error in job: %v", err) + log.Panicf("error in job: %v", err) } } diff --git a/gen_handler_tests b/gen_handler_tests deleted file mode 100755 index a295138..0000000 --- a/gen_handler_tests +++ /dev/null @@ -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 diff --git a/go.mod b/go.mod index ad6ccb4..91310df 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,8 @@ require ( github.com/cespare/xxhash v1.1.0 github.com/disintegration/imaging v1.6.2 github.com/dustin/go-humanize v1.0.0 - github.com/faiface/beep v1.0.3-0.20210817042730-1c98bf641535 - github.com/google/uuid v1.1.2 // indirect - github.com/gopherjs/gopherwasm v1.0.0 // indirect + github.com/faiface/beep v1.1.0 + github.com/google/uuid v1.3.0 // indirect github.com/gorilla/mux v1.8.0 github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.2.1 @@ -21,23 +20,21 @@ require ( github.com/jinzhu/gorm v1.9.16 github.com/josephburnett/jd v0.0.0-20191228205456-aa1a7c66b42f github.com/karrick/godirwalk v1.16.1 - github.com/kr/pretty v0.1.0 // indirect - github.com/mewkiz/pkg v0.0.0-20200702171441-dd47075182ea // indirect - github.com/mitchellh/copystructure v1.0.0 // indirect - github.com/mitchellh/reflectwalk v1.0.1 // indirect - github.com/mmcdole/gofeed v1.1.0 + github.com/matryer/is v1.4.0 + github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mmcdole/gofeed v1.1.3 + github.com/mmcdole/goxpp v0.0.0-20200921145534-2f3784f67354 // indirect github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd github.com/oklog/run v1.1.0 github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c 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/wader/gormstore v0.0.0-20200328121358-65a111a20c23 - golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect - golang.org/x/exp v0.0.0-20201229011636-eab1b5eb1a03 // indirect - golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect - golang.org/x/sys v0.0.0-20201223074533-0d417f636930 // indirect - gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + github.com/wader/gormstore v0.0.0-20211009162750-8bf4f5606ef4 + golang.org/x/exp v0.0.0-20211103171733-83d51122435b // indirect + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect + golang.org/x/mobile v0.0.0-20211103151657-e68c98865fb2 // indirect + golang.org/x/net v0.0.0-20211104170005-ce137452f963 // indirect + golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect gopkg.in/gormigrate.v1 v1.6.0 ) diff --git a/go.sum b/go.sum index c26f371..a632291 100644 --- a/go.sum +++ b/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/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/faiface/beep v1.0.2 h1:UB5DiRNmA4erfUYnHbgU4UB6DlBOrsdEFRtcc8sCkdQ= -github.com/faiface/beep v1.0.2/go.mod h1:1yLb5yRdHMsovYYWVqYLioXkVuziCSITW1oarTeduQM= -github.com/faiface/beep v1.0.3-0.20210817042730-1c98bf641535 h1:391d1LXITcjNUsoeXUY21E5UCsmFz/W3ft9sInjynDI= -github.com/faiface/beep v1.0.3-0.20210817042730-1c98bf641535/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4= +github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c= +github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +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/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ= github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= @@ -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/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/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/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 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.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 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.1 h1:pn/SKU1+/rfK8KaZXdGEC2G/KCB2aLRjbTCrwKcokao= 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.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/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= 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/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= -github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +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/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= 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 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.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/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 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/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= 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.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/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/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-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 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/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/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= 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/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 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/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= -github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mmcdole/gofeed v1.1.0 h1:T2WrGLVJRV04PY2qwhEJLHCt9JiCtBhb6SmC8ZvJH08= -github.com/mmcdole/gofeed v1.1.0/go.mod h1:PPiVwgDXLlz2N83KB4TrIim2lyYM5Zn7ZWH9Pi4oHUk= -github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +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/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/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/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= @@ -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-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= 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-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/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-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-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/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-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 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/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-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-20190311183353-d8887717615a/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/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 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-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-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-20190626150813-e07cf5db2756/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= 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= -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 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/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/multierr/multierr.go b/multierr/multierr.go index 04bd4d9..d5946b0 100644 --- a/multierr/multierr.go +++ b/multierr/multierr.go @@ -5,11 +5,12 @@ import "strings" type Err []error func (me Err) Error() string { - var strs []string + var builder strings.Builder 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 { diff --git a/server/ctrladmin/handlers.go b/server/ctrladmin/handlers.go index de725cf..d1dcb8b 100644 --- a/server/ctrladmin/handlers.go +++ b/server/ctrladmin/handlers.go @@ -27,7 +27,7 @@ func firstExisting(or string, strings ...string) string { func doScan(scanner *scanner.Scanner, opts scanner.ScanOptions) { go func() { - if err := scanner.Start(opts); err != nil { + if err := scanner.ScanAndClean(opts); err != nil { 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 { data := &templateData{} - // ** begin stats box - c.DB.Table("artists").Count(&data.ArtistCount) - c.DB.Table("albums").Count(&data.AlbumCount) + // stats box + c.DB.Model(&db.Artist{}).Count(&data.ArtistCount) + c.DB.Model(&db.Album{}).Count(&data.AlbumCount) c.DB.Table("tracks").Count(&data.TrackCount) - // ** begin lastfm box + // lastfm box scheme := firstExisting( "http", // fallback r.Header.Get("X-Forwarded-Proto"), @@ -60,36 +60,37 @@ func (c *Controller) ServeHome(r *http.Request) *Response { r.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 - // ** begin users box + // users box c.DB.Find(&data.AllUsers) - // ** begin recent folders box + // recent folders box c.DB. Where("tag_artist_id IS NOT NULL"). Order("modified_at DESC"). Limit(8). Find(&data.RecentFolders) - data.IsScanning = scanner.IsScanning() - if tStr := c.DB.GetSetting("last_scan_time"); tStr != "" { + data.IsScanning = c.Scanner.IsScanning() + if tStr, err := c.DB.GetSetting("last_scan_time"); err != nil { i, _ := strconv.ParseInt(tStr, 10, 64) data.LastScanTime = time.Unix(i, 0) } - // + user := r.Context().Value(CtxUser).(*db.User) - // ** begin playlists box + + // playlists box c.DB. Where("user_id=?", user.ID). Limit(20). Find(&data.Playlists) - // ** begin transcoding box + // transcoding box c.DB. Where("user_id=?", user.ID). Find(&data.TranscodePreferences) for profile := range encode.Profiles() { data.TranscodeProfiles = append(data.TranscodeProfiles, profile) } - // ** begin podcasts box + // podcasts box c.DB.Find(&data.Podcasts) // return &Response{ @@ -143,11 +144,15 @@ func (c *Controller) ServeLinkLastFMDo(r *http.Request) *Response { code: 400, } } - sessionKey, err := lastfm.GetSession( - c.DB.GetSetting("lastfm_api_key"), - c.DB.GetSetting("lastfm_secret"), - token, - ) + apiKey, err := c.DB.GetSetting("lastfm_api_key") + if err != nil { + return &Response{code: 500, err: fmt.Sprintf("couldn't get api key: %v", err)} + } + 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 { return &Response{ redirect: "/admin/home", @@ -341,8 +346,13 @@ func (c *Controller) ServeCreateUserDo(r *http.Request) *Response { func (c *Controller) ServeUpdateLastFMAPIKey(r *http.Request) *Response { data := &templateData{} - data.CurrentLastFMAPIKey = c.DB.GetSetting("lastfm_api_key") - data.CurrentLastFMAPISecret = c.DB.GetSetting("lastfm_secret") + var err error + 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{ template: "update_lastfm_api_key.tmpl", data: data, @@ -358,8 +368,12 @@ func (c *Controller) ServeUpdateLastFMAPIKeyDo(r *http.Request) *Response { flashW: []string{err.Error()}, } } - c.DB.SetSetting("lastfm_api_key", apiKey) - c.DB.SetSetting("lastfm_secret", secret) + if err := c.DB.SetSetting("lastfm_api_key", apiKey); err != nil { + 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"} } diff --git a/server/ctrladmin/handlers_playlist.go b/server/ctrladmin/handlers_playlist.go index f337572..6d7e185 100644 --- a/server/ctrladmin/handlers_playlist.go +++ b/server/ctrladmin/handlers_playlist.go @@ -30,7 +30,7 @@ func playlistParseLine(c *Controller, path string) (int, error) { c.MusicPath, path) err := query.First(&track).Error switch { - case gorm.IsRecordNotFoundError(err): + case errors.Is(err, gorm.ErrRecordNotFound): return 0, fmt.Errorf("%v: %w", err, errPlaylistNoMatch) case err != nil: return 0, fmt.Errorf("while matching: %w", err) diff --git a/server/ctrlsubsonic/ctrl.go b/server/ctrlsubsonic/ctrl.go index 7e5784c..1b544cf 100644 --- a/server/ctrlsubsonic/ctrl.go +++ b/server/ctrlsubsonic/ctrl.go @@ -29,6 +29,7 @@ type Controller struct { *ctrlbase.Controller CachePath string CoverCachePath string + PodcastsPath string Jukebox *jukebox.Jukebox Scrobblers []scrobble.Scrobbler Podcasts *podcasts.Podcasts diff --git a/server/ctrlsubsonic/ctrl_test.go b/server/ctrlsubsonic/ctrl_test.go index f661c96..3200a2b 100644 --- a/server/ctrlsubsonic/ctrl_test.go +++ b/server/ctrlsubsonic/ctrl_test.go @@ -2,6 +2,7 @@ package ctrlsubsonic import ( "context" + "io/ioutil" "log" "net/http" "net/http/httptest" @@ -16,14 +17,12 @@ import ( "go.senan.xyz/gonic/server/ctrlbase" "go.senan.xyz/gonic/server/ctrlsubsonic/params" - "go.senan.xyz/gonic/server/db" + "go.senan.xyz/gonic/server/mockfs" ) var ( - testDataDir = "testdata" - testCamelExpr = regexp.MustCompile("([a-z0-9])([A-Z])") - testDBPath = path.Join(testDataDir, "db") - testController *Controller + testDataDir = "testdata" + testCamelExpr = regexp.MustCompile("([a-z0-9])([A-Z])") ) type queryCase struct { @@ -53,18 +52,26 @@ func makeHTTPMock(query url.Values) (*httptest.ResponseRecorder, *http.Request) 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 { qc := qc // pin t.Run(qc.expectPath, func(t *testing.T) { t.Parallel() rr, req := makeHTTPMock(qc.params) - testController.H(h).ServeHTTP(rr, req) + contr.H(h).ServeHTTP(rr, req) body := rr.Body.String() if status := rr.Code; status != http.StatusOK { t.Fatalf("didn't give a 200\n%s", body) } 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 expected, err := jd.ReadJsonFile(goldenPath) 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) { - db, err := db.New(testDBPath) - if err != nil { - log.Fatalf("error opening database: %v\n", err) - } - testController = &Controller{ - Controller: &ctrlbase.Controller{DB: db}, - } + log.SetOutput(ioutil.Discard) os.Exit(m.Run()) } diff --git a/server/ctrlsubsonic/handlers_bookmark.go b/server/ctrlsubsonic/handlers_bookmark.go index 8595205..85af490 100644 --- a/server/ctrlsubsonic/handlers_bookmark.go +++ b/server/ctrlsubsonic/handlers_bookmark.go @@ -1,6 +1,7 @@ package ctrlsubsonic import ( + "errors" "net/http" "github.com/jinzhu/gorm" @@ -18,7 +19,7 @@ func (c *Controller) ServeGetBookmarks(r *http.Request) *spec.Response { Where("user_id=?", user.ID). Find(&bookmarks). Error - if gorm.IsRecordNotFoundError(err) { + if errors.Is(err, gorm.ErrRecordNotFound) { return spec.NewResponse() } sub := spec.NewResponse() diff --git a/server/ctrlsubsonic/handlers_by_folder.go b/server/ctrlsubsonic/handlers_by_folder.go index 59687cf..080e767 100644 --- a/server/ctrlsubsonic/handlers_by_folder.go +++ b/server/ctrlsubsonic/handlers_by_folder.go @@ -61,7 +61,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response { childrenObj := []*spec.TrackChild{} folder := &db.Album{} 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 c.DB. Where("parent_id=?", id.Value). @@ -70,7 +70,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response { for _, c := range childFolders { 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 c.DB. Where("album_id=?", id.Value). @@ -86,7 +86,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response { } childrenObj = append(childrenObj, toAppend) } - // ** begin respond section + // respond section sub := spec.NewResponse() sub.Directory = spec.NewDirectoryByFolder(folder, childrenObj) return sub @@ -167,7 +167,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response { } query = fmt.Sprintf("%%%s%%", strings.TrimSuffix(query, "*")) results := &spec.SearchResultTwo{} - // ** begin search "artists" + // search "artists" var artists []*db.Album c.DB. Where(` @@ -182,7 +182,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response { results.Artists = append(results.Artists, spec.NewDirectoryByFolder(a, nil)) } - // ** begin search "albums" + // search "albums" var albums []*db.Album c.DB. Where(` @@ -196,7 +196,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response { for _, a := range albums { results.Albums = append(results.Albums, spec.NewTCAlbumByFolder(a)) } - // ** begin search tracks + // search tracks var tracks []*db.Track c.DB. Preload("Album"). diff --git a/server/ctrlsubsonic/handlers_by_folder_test.go b/server/ctrlsubsonic/handlers_by_folder_test.go index 0ff9a3c..0cfe691 100644 --- a/server/ctrlsubsonic/handlers_by_folder_test.go +++ b/server/ctrlsubsonic/handlers_by_folder_test.go @@ -8,20 +8,30 @@ import ( ) 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}, }) } 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-3"}}, "with_tracks", false}, }) } 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": {"alphabeticalByName"}}, "alpha_name", false}, {url.Values{"type": {"newest"}}, "newest", false}, @@ -30,9 +40,13 @@ func TestGetAlbumList(t *testing.T) { } func TestSearchTwo(t *testing.T) { - runQueryCases(t, testController.ServeSearchTwo, []*queryCase{ - {url.Values{"query": {"13"}}, "q_13", false}, - {url.Values{"query": {"ani"}}, "q_ani", false}, - {url.Values{"query": {"cert"}}, "q_cert", false}, + t.Parallel() + contr, m := makeController(t) + defer m.CleanUp() + + 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}, }) } diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go index 6424b9f..9c19bc6 100644 --- a/server/ctrlsubsonic/handlers_by_tags.go +++ b/server/ctrlsubsonic/handlers_by_tags.go @@ -1,6 +1,7 @@ package ctrlsubsonic import ( + "errors" "fmt" "net/http" "strings" @@ -88,7 +89,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response { }). First(album, id.Value). Error - if gorm.IsRecordNotFoundError(err) { + if errors.Is(err, gorm.ErrRecordNotFound) { return spec.NewError(10, "couldn't find an album with that id") } sub := spec.NewResponse() @@ -174,7 +175,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response { query = fmt.Sprintf("%%%s%%", strings.TrimSuffix(query, "*")) results := &spec.SearchResultThree{} - // ** begin search "artists" + // search "artists" var artists []*db.Artist c.DB. 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, spec.NewArtistByTags(a)) } - // ** begin search "albums" + // search "albums" var albums []*db.Album c.DB. Preload("TagArtist"). @@ -199,7 +200,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response { results.Albums = append(results.Albums, spec.NewAlbumByTags(a, a.TagArtist)) } - // ** begin search tracks + // search tracks var tracks []*db.Track c.DB. Preload("Album"). @@ -223,7 +224,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { if err != nil { 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 == "" { sub := spec.NewResponse() sub.ArtistInfoTwo = &spec.ArtistInfo{} @@ -234,7 +235,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { Where("id=?", id.Value). Find(artist). Error - if gorm.IsRecordNotFoundError(err) { + if errors.Is(err, gorm.ErrRecordNotFound) { return spec.NewError(70, "artist with id `%s` not found", id) } info, err := lastfm.ArtistGetInfo(apiKey, artist) @@ -271,7 +272,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { Group("artists.id"). Find(artist). Error - if gorm.IsRecordNotFoundError(err) && !inclNotPresent { + if errors.Is(err, gorm.ErrRecordNotFound) && !inclNotPresent { continue } similar := &spec.SimilarArtist{ diff --git a/server/ctrlsubsonic/handlers_by_tags_test.go b/server/ctrlsubsonic/handlers_by_tags_test.go index 326586e..55f9ed9 100644 --- a/server/ctrlsubsonic/handlers_by_tags_test.go +++ b/server/ctrlsubsonic/handlers_by_tags_test.go @@ -6,13 +6,21 @@ import ( ) 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}, }) } 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-2"}}, "id_two", false}, {url.Values{"id": {"ar-3"}}, "id_three", false}, @@ -20,14 +28,22 @@ func TestGetArtist(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-3"}}, "with_cover", false}, }) } 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": {"alphabeticalByName"}}, "alpha_name", false}, {url.Values{"type": {"newest"}}, "newest", false}, @@ -36,9 +52,13 @@ func TestGetAlbumListTwo(t *testing.T) { } func TestSearchThree(t *testing.T) { - runQueryCases(t, testController.ServeSearchThree, []*queryCase{ - {url.Values{"query": {"13"}}, "q_13", false}, - {url.Values{"query": {"ani"}}, "q_ani", false}, - {url.Values{"query": {"cert"}}, "q_cert", false}, + t.Parallel() + contr, m := makeController(t) + defer m.CleanUp() + + 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}, }) } diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go index 11ad1b6..99c6522 100644 --- a/server/ctrlsubsonic/handlers_common.go +++ b/server/ctrlsubsonic/handlers_common.go @@ -1,6 +1,7 @@ package ctrlsubsonic import ( + "errors" "log" "net/http" "time" @@ -80,7 +81,7 @@ func (c *Controller) ServeGetMusicFolders(r *http.Request) *spec.Response { func (c *Controller) ServeStartScan(r *http.Request) *spec.Response { 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) } }() @@ -95,7 +96,7 @@ func (c *Controller) ServeGetScanStatus(r *http.Request) *spec.Response { sub := spec.NewResponse() sub.ScanStatus = &spec.ScanStatus{ - Scanning: scanner.IsScanning(), + Scanning: c.Scanner.IsScanning(), Count: trackCount, } return sub @@ -129,7 +130,7 @@ func (c *Controller) ServeGetPlayQueue(r *http.Request) *spec.Response { Where("user_id=?", user.ID). Find(&queue). Error - if gorm.IsRecordNotFoundError(err) { + if errors.Is(err, gorm.ErrRecordNotFound) { return spec.NewResponse() } sub := spec.NewResponse() @@ -188,7 +189,7 @@ func (c *Controller) ServeGetSong(r *http.Request) *spec.Response { Preload("Album"). First(track). Error - if gorm.IsRecordNotFoundError(err) { + if errors.Is(err, gorm.ErrRecordNotFound) { return spec.NewError(10, "couldn't find a track with that id") } sub := spec.NewResponse() diff --git a/server/ctrlsubsonic/handlers_playlist.go b/server/ctrlsubsonic/handlers_playlist.go index 3b31ea2..5ef22a2 100644 --- a/server/ctrlsubsonic/handlers_playlist.go +++ b/server/ctrlsubsonic/handlers_playlist.go @@ -1,6 +1,7 @@ package ctrlsubsonic import ( + "errors" "log" "net/http" "sort" @@ -33,7 +34,7 @@ func playlistRender(c *Controller, playlist *db.Playlist) *spec.Playlist { Preload("Album"). Find(&track). Error - if gorm.IsRecordNotFoundError(err) { + if errors.Is(err, gorm.ErrRecordNotFound) { log.Printf("wasn't able to find track with id %d", id) continue } @@ -68,7 +69,7 @@ func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response { Where("id=?", playlistID). Find(&playlist). Error - if gorm.IsRecordNotFoundError(err) { + if errors.Is(err, gorm.ErrRecordNotFound) { return spec.NewError(70, "playlist with id `%d` not found", playlistID) } sub := spec.NewResponse() diff --git a/server/ctrlsubsonic/handlers_raw.go b/server/ctrlsubsonic/handlers_raw.go index 776a000..73a1d3e 100644 --- a/server/ctrlsubsonic/handlers_raw.go +++ b/server/ctrlsubsonic/handlers_raw.go @@ -173,7 +173,7 @@ func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *s _, err = os.Stat(cachePath) switch { 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 { 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: podcast, err := streamGetPodcast(c.DB, id.Value) audioFile = podcast - audioPath = path.Join(c.Podcasts.PodcastBasePath, podcast.Path) + audioPath = path.Join(c.PodcastsPath, podcast.Path) if err != nil { 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: podcast, err := streamGetPodcast(c.DB, id.Value) audioFile = podcast - filePath = path.Join(c.Podcasts.PodcastBasePath, podcast.Path) + filePath = path.Join(c.PodcastsPath, podcast.Path) if err != nil { return spec.NewError(70, "podcast with id `%s` was not found", id) } diff --git a/server/ctrlsubsonic/testdata/db b/server/ctrlsubsonic/testdata/db deleted file mode 100644 index 7aad42f..0000000 Binary files a/server/ctrlsubsonic/testdata/db and /dev/null differ diff --git a/server/ctrlsubsonic/testdata/test_get_album_list_alpha_artist b/server/ctrlsubsonic/testdata/test_get_album_list_alpha_artist index d948223..8d2cca6 100644 --- a/server/ctrlsubsonic/testdata/test_get_album_list_alpha_artist +++ b/server/ctrlsubsonic/testdata/test_get_album_list_alpha_artist @@ -6,133 +6,121 @@ "albumList": { "album": [ { - "id": "al-8", - "coverArt": "al-8", - "artist": "13th Floor Lowervators", - "title": "(1967) Easter Nowhere", + "id": "al-2", + "coverArt": "al-2", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", + "title": "album-0", "album": "", - "parent": "al-7", + "parent": "al-1", "isDir": true, "name": "", - "songCount": 10, - "duration": 2609, - "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" + "songCount": 3, + "duration": 300 }, { "id": "al-3", "coverArt": "al-3", - "artist": "Jah Wobble, The Edge, Holger Czukay", - "title": "(1983) Snake Charmer", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", + "title": "album-1", "album": "", - "parent": "al-2", + "parent": "al-1", "isDir": true, "name": "", - "songCount": 5, - "duration": 1871, - "created": "2019-05-16T22:10:52+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-16", - "coverArt": "al-16", - "artist": "Swell Maps", - "title": "(1980) Jane From Occupied Europe", + "id": "al-4", + "coverArt": "al-4", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", + "title": "album-2", "album": "", - "parent": "al-15", + "parent": "al-1", "isDir": true, "name": "", - "songCount": 16, - "duration": 3040, - "created": "2019-04-30T16:48:48+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-17", - "coverArt": "al-17", - "artist": "Swell Maps", - "title": "(1979) A Trip to Marineville", + "id": "al-7", + "coverArt": "al-7", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", + "title": "album-0", "album": "", - "parent": "al-15", + "parent": "al-6", "isDir": true, "name": "", - "songCount": 18, - "duration": 3266, - "created": "2019-04-30T16:48:48+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-19", - "coverArt": "al-19", - "artist": "Ten Years After", - "title": "(1967) Ten Years After", + "id": "al-8", + "coverArt": "al-8", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", + "title": "album-1", "album": "", - "parent": "al-18", + "parent": "al-6", "isDir": true, "name": "", - "songCount": 15, - "duration": 3812, - "created": "2019-04-30T16:48:30+01:00" + "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-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": "There", - "title": "(2010) Anika", + "artist": "artist-2", + "created": "2019-11-30T00:00:00Z", + "title": "album-2", "album": "", - "parent": "al-12", + "parent": "al-10", "isDir": true, "name": "", - "songCount": 9, - "duration": 2169, - "created": "2019-05-23T15:12:02.921473302+01:00" + "songCount": 3, + "duration": 300 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_album_list_alpha_name b/server/ctrlsubsonic/testdata/test_get_album_list_alpha_name index 3dde97e..4e04828 100644 --- a/server/ctrlsubsonic/testdata/test_get_album_list_alpha_name +++ b/server/ctrlsubsonic/testdata/test_get_album_list_alpha_name @@ -6,133 +6,121 @@ "albumList": { "album": [ { - "id": "al-9", - "coverArt": "al-9", - "artist": "13th Floor Lowervators", - "title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators", + "id": "al-2", + "coverArt": "al-2", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", + "title": "album-0", "album": "", - "parent": "al-7", + "parent": "al-1", "isDir": true, "name": "", - "songCount": 21, - "duration": 4222, - "created": "2019-06-13T12:57:24.306717554+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-8", - "coverArt": "al-8", - "artist": "13th Floor Lowervators", - "title": "(1967) Easter Nowhere", + "id": "al-7", + "coverArt": "al-7", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", + "title": "album-0", "album": "", - "parent": "al-7", + "parent": "al-6", "isDir": true, "name": "", - "songCount": 10, - "duration": 2609, - "created": "2019-06-13T12:57:28.850090338+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-19", - "coverArt": "al-19", - "artist": "Ten Years After", - "title": "(1967) Ten Years After", + "id": "al-11", + "coverArt": "al-11", + "artist": "artist-2", + "created": "2019-11-30T00:00:00Z", + "title": "album-0", "album": "", - "parent": "al-18", + "parent": "al-10", "isDir": true, "name": "", - "songCount": 15, - "duration": 3812, - "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" + "songCount": 3, + "duration": 300 }, { "id": "al-3", "coverArt": "al-3", - "artist": "Jah Wobble, The Edge, Holger Czukay", - "title": "(1983) Snake Charmer", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", + "title": "album-1", "album": "", - "parent": "al-2", + "parent": "al-1", "isDir": true, "name": "", - "songCount": 5, - "duration": 1871, - "created": "2019-05-16T22:10:52+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-5", - "coverArt": "al-5", - "artist": "A Certain Ratio", - "title": "(1994) The Graveyard and the Ballroom", + "id": "al-8", + "coverArt": "al-8", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", + "title": "album-1", "album": "", - "parent": "al-4", + "parent": "al-6", "isDir": true, "name": "", - "songCount": 14, - "duration": 2738, - "created": "2019-06-05T17:46:37.675917974+01:00" + "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-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", "coverArt": "al-13", - "artist": "There", - "title": "(2010) Anika", + "artist": "artist-2", + "created": "2019-11-30T00:00:00Z", + "title": "album-2", "album": "", - "parent": "al-12", + "parent": "al-10", "isDir": true, "name": "", - "songCount": 9, - "duration": 2169, - "created": "2019-05-23T15:12:02.921473302+01:00" + "songCount": 3, + "duration": 300 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_album_list_newest b/server/ctrlsubsonic/testdata/test_get_album_list_newest index 91c8d04..8d2cca6 100644 --- a/server/ctrlsubsonic/testdata/test_get_album_list_newest +++ b/server/ctrlsubsonic/testdata/test_get_album_list_newest @@ -6,133 +6,121 @@ "albumList": { "album": [ { - "id": "al-8", - "coverArt": "al-8", - "artist": "13th Floor Lowervators", - "title": "(1967) Easter Nowhere", + "id": "al-2", + "coverArt": "al-2", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", + "title": "album-0", "album": "", - "parent": "al-7", + "parent": "al-1", "isDir": true, "name": "", - "songCount": 10, - "duration": 2609, - "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" + "songCount": 3, + "duration": 300 }, { "id": "al-3", "coverArt": "al-3", - "artist": "Jah Wobble, The Edge, Holger Czukay", - "title": "(1983) Snake Charmer", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", + "title": "album-1", "album": "", - "parent": "al-2", + "parent": "al-1", "isDir": true, "name": "", - "songCount": 5, - "duration": 1871, - "created": "2019-05-16T22:10:52+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-16", - "coverArt": "al-16", - "artist": "Swell Maps", - "title": "(1980) Jane From Occupied Europe", + "id": "al-4", + "coverArt": "al-4", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", + "title": "album-2", "album": "", - "parent": "al-15", + "parent": "al-1", "isDir": true, "name": "", - "songCount": 16, - "duration": 3040, - "created": "2019-04-30T16:48:48+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-17", - "coverArt": "al-17", - "artist": "Swell Maps", - "title": "(1979) A Trip to Marineville", + "id": "al-7", + "coverArt": "al-7", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", + "title": "album-0", "album": "", - "parent": "al-15", + "parent": "al-6", "isDir": true, "name": "", - "songCount": 18, - "duration": 3266, - "created": "2019-04-30T16:48:48+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-19", - "coverArt": "al-19", - "artist": "Ten Years After", - "title": "(1967) Ten Years After", + "id": "al-8", + "coverArt": "al-8", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", + "title": "album-1", "album": "", - "parent": "al-18", + "parent": "al-6", "isDir": true, "name": "", - "songCount": 15, - "duration": 3812, - "created": "2019-04-30T16:48:30+01:00" + "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-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 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_album_list_random b/server/ctrlsubsonic/testdata/test_get_album_list_random index bf5c8b1..409cb21 100644 --- a/server/ctrlsubsonic/testdata/test_get_album_list_random +++ b/server/ctrlsubsonic/testdata/test_get_album_list_random @@ -6,133 +6,121 @@ "albumList": { "album": [ { - "id": "al-19", - "coverArt": "al-19", - "artist": "Ten Years After", - "title": "(1967) Ten Years After", + "id": "al-2", + "coverArt": "al-2", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", + "title": "album-0", "album": "", - "parent": "al-18", + "parent": "al-1", "isDir": true, "name": "", - "songCount": 15, - "duration": 3812, - "created": "2019-04-30T16:48:30+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-21", - "coverArt": "al-21", - "artist": "Captain Beefheart", - "title": "(1970) Lick My Decals Off, Bitch", + "id": "al-7", + "coverArt": "al-7", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", + "title": "album-0", "album": "", - "parent": "al-20", + "parent": "al-6", "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-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" + "songCount": 3, + "duration": 300 }, { "id": "al-13", "coverArt": "al-13", - "artist": "There", - "title": "(2010) Anika", + "artist": "artist-2", + "created": "2019-11-30T00:00:00Z", + "title": "album-2", "album": "", - "parent": "al-12", + "parent": "al-10", "isDir": true, "name": "", - "songCount": 9, - "duration": 2169, - "created": "2019-05-23T15:12:02.921473302+01:00" + "songCount": 3, + "duration": 300 }, { - "id": "al-8", - "coverArt": "al-8", - "artist": "13th Floor Lowervators", - "title": "(1967) Easter Nowhere", + "id": "al-11", + "coverArt": "al-11", + "artist": "artist-2", + "created": "2019-11-30T00:00:00Z", + "title": "album-0", "album": "", - "parent": "al-7", + "parent": "al-10", "isDir": true, "name": "", - "songCount": 10, - "duration": 2609, - "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" + "songCount": 3, + "duration": 300 }, { "id": "al-3", "coverArt": "al-3", - "artist": "Jah Wobble, The Edge, Holger Czukay", - "title": "(1983) Snake Charmer", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", + "title": "album-1", "album": "", - "parent": "al-2", + "parent": "al-1", "isDir": true, "name": "", - "songCount": 5, - "duration": 1871, - "created": "2019-05-16T22:10:52+01:00" + "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-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 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_artist b/server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_artist index 32d9294..0b2abfa 100644 --- a/server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_artist +++ b/server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_artist @@ -6,133 +6,121 @@ "albumList2": { "album": [ { - "id": "al-8", - "coverArt": "al-8", - "artistId": "ar-3", - "artist": "13th Floor Elevators", + "id": "al-2", + "coverArt": "al-2", + "artistId": "ar-1", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Easter Everywhere", - "songCount": 10, - "duration": 2609, - "created": "2019-06-13T12:57:28.850090338+01:00", - "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 + "name": "album-0", + "songCount": 3, + "duration": 300, + "year": 2021 }, { "id": "al-3", "coverArt": "al-3", "artistId": "ar-1", - "artist": "Jah Wobble, The Edge & Holger Czukay", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Snake Charmer", - "songCount": 5, - "duration": 1871, - "created": "2019-05-16T22:10:52+01:00", - "year": 1983 + "name": "album-1", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-16", - "coverArt": "al-16", - "artistId": "ar-5", - "artist": "Swell Maps", + "id": "al-4", + "coverArt": "al-4", + "artistId": "ar-1", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Jane From Occupied Europe", - "songCount": 16, - "duration": 3040, - "created": "2019-04-30T16:48:48+01:00", - "year": 1980 + "name": "album-2", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-17", - "coverArt": "al-17", - "artistId": "ar-5", - "artist": "Swell Maps", + "id": "al-7", + "coverArt": "al-7", + "artistId": "ar-2", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "A Trip to Marineville", - "songCount": 18, - "duration": 3266, - "created": "2019-04-30T16:48:48+01:00", - "year": 1979 + "name": "album-0", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-19", - "coverArt": "al-19", - "artistId": "ar-6", - "artist": "Ten Years After", + "id": "al-8", + "coverArt": "al-8", + "artistId": "ar-2", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Ten Years After", - "songCount": 15, - "duration": 3812, - "created": "2019-04-30T16:48:30+01:00", - "year": 1967 + "name": "album-1", + "songCount": 3, + "duration": 300, + "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": 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 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_name b/server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_name index d743286..2c80d21 100644 --- a/server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_name +++ b/server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_name @@ -6,133 +6,121 @@ "albumList2": { "album": [ { - "id": "al-17", - "coverArt": "al-17", - "artistId": "ar-5", - "artist": "Swell Maps", + "id": "al-2", + "coverArt": "al-2", + "artistId": "ar-1", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "A Trip to Marineville", - "songCount": 18, - "duration": 3266, - "created": "2019-04-30T16:48:48+01:00", - "year": 1979 + "name": "album-0", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-13", - "coverArt": "al-13", - "artistId": "ar-4", - "artist": "Anikas", + "id": "al-7", + "coverArt": "al-7", + "artistId": "ar-2", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Anika", - "songCount": 9, - "duration": 2169, - "created": "2019-05-23T15:12:02.921473302+01:00", - "year": 2010 + "name": "album-0", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-8", - "coverArt": "al-8", + "id": "al-11", + "coverArt": "al-11", "artistId": "ar-3", - "artist": "13th Floor Elevators", + "artist": "artist-2", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Easter Everywhere", - "songCount": 10, - "duration": 2609, - "created": "2019-06-13T12:57:28.850090338+01:00", - "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 + "name": "album-0", + "songCount": 3, + "duration": 300, + "year": 2021 }, { "id": "al-3", "coverArt": "al-3", "artistId": "ar-1", - "artist": "Jah Wobble, The Edge & Holger Czukay", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Snake Charmer", - "songCount": 5, - "duration": 1871, - "created": "2019-05-16T22:10:52+01:00", - "year": 1983 + "name": "album-1", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "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-5", - "coverArt": "al-5", + "id": "al-8", + "coverArt": "al-8", "artistId": "ar-2", - "artist": "A Certain Ratio", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "The Graveyard and the Ballroom", - "songCount": 14, - "duration": 2738, - "created": "2019-06-05T17:46:37.675917974+01:00", - "year": 1994 + "name": "album-1", + "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-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", "coverArt": "al-9", - "artistId": "ar-3", - "artist": "13th Floor Elevators", + "artistId": "ar-2", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", "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 + "name": "album-2", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-6", - "artistId": "ar-2", - "artist": "A Certain Ratio", + "id": "al-13", + "coverArt": "al-13", + "artistId": "ar-3", + "artist": "artist-2", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "To Each...", - "songCount": 9, - "duration": 2801, - "created": "2019-05-23T15:12:02.921473302+01:00", - "year": 1981 + "name": "album-2", + "songCount": 3, + "duration": 300, + "year": 2021 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_album_list_two_newest b/server/ctrlsubsonic/testdata/test_get_album_list_two_newest index 09ebebf..0b2abfa 100644 --- a/server/ctrlsubsonic/testdata/test_get_album_list_two_newest +++ b/server/ctrlsubsonic/testdata/test_get_album_list_two_newest @@ -6,133 +6,121 @@ "albumList2": { "album": [ { - "id": "al-8", - "coverArt": "al-8", - "artistId": "ar-3", - "artist": "13th Floor Elevators", + "id": "al-2", + "coverArt": "al-2", + "artistId": "ar-1", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Easter Everywhere", - "songCount": 10, - "duration": 2609, - "created": "2019-06-13T12:57:28.850090338+01:00", - "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 + "name": "album-0", + "songCount": 3, + "duration": 300, + "year": 2021 }, { "id": "al-3", "coverArt": "al-3", "artistId": "ar-1", - "artist": "Jah Wobble, The Edge & Holger Czukay", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Snake Charmer", - "songCount": 5, - "duration": 1871, - "created": "2019-05-16T22:10:52+01:00", - "year": 1983 + "name": "album-1", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-16", - "coverArt": "al-16", - "artistId": "ar-5", - "artist": "Swell Maps", + "id": "al-4", + "coverArt": "al-4", + "artistId": "ar-1", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Jane From Occupied Europe", - "songCount": 16, - "duration": 3040, - "created": "2019-04-30T16:48:48+01:00", - "year": 1980 + "name": "album-2", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-17", - "coverArt": "al-17", - "artistId": "ar-5", - "artist": "Swell Maps", + "id": "al-7", + "coverArt": "al-7", + "artistId": "ar-2", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "A Trip to Marineville", - "songCount": 18, - "duration": 3266, - "created": "2019-04-30T16:48:48+01:00", - "year": 1979 + "name": "album-0", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-19", - "coverArt": "al-19", - "artistId": "ar-6", - "artist": "Ten Years After", + "id": "al-8", + "coverArt": "al-8", + "artistId": "ar-2", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Ten Years After", - "songCount": 15, - "duration": 3812, - "created": "2019-04-30T16:48:30+01:00", - "year": 1967 + "name": "album-1", + "songCount": 3, + "duration": 300, + "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": 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 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_album_list_two_random b/server/ctrlsubsonic/testdata/test_get_album_list_two_random index 0b6ba98..053ed35 100644 --- a/server/ctrlsubsonic/testdata/test_get_album_list_two_random +++ b/server/ctrlsubsonic/testdata/test_get_album_list_two_random @@ -6,133 +6,121 @@ "albumList2": { "album": [ { - "id": "al-17", - "coverArt": "al-17", - "artistId": "ar-5", - "artist": "Swell Maps", + "id": "al-3", + "coverArt": "al-3", + "artistId": "ar-1", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "A Trip to Marineville", - "songCount": 18, - "duration": 3266, - "created": "2019-04-30T16:48:48+01:00", - "year": 1979 + "name": "album-1", + "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-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", "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", - "artist": "A Certain Ratio", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "To Each...", - "songCount": 9, - "duration": 2801, - "created": "2019-05-23T15:12:02.921473302+01:00", - "year": 1981 + "name": "album-2", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-8", - "coverArt": "al-8", - "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", + "id": "al-2", + "coverArt": "al-2", "artistId": "ar-1", - "artist": "Jah Wobble, The Edge & Holger Czukay", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Snake Charmer", - "songCount": 5, - "duration": 1871, - "created": "2019-05-16T22:10:52+01:00", - "year": 1983 + "name": "album-0", + "songCount": 3, + "duration": 300, + "year": 2021 }, { "id": "al-13", "coverArt": "al-13", - "artistId": "ar-4", - "artist": "Anikas", + "artistId": "ar-3", + "artist": "artist-2", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Anika", - "songCount": 9, - "duration": 2169, - "created": "2019-05-23T15:12:02.921473302+01:00", - "year": 2010 + "name": "album-2", + "songCount": 3, + "duration": 300, + "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": 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 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_album_with_cover b/server/ctrlsubsonic/testdata/test_get_album_with_cover index 837e2ad..1b7d161 100644 --- a/server/ctrlsubsonic/testdata/test_get_album_with_cover +++ b/server/ctrlsubsonic/testdata/test_get_album_with_cover @@ -7,129 +7,80 @@ "id": "al-3", "coverArt": "al-3", "artistId": "ar-1", - "artist": "Jah Wobble, The Edge & Holger Czukay", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Snake Charmer", - "songCount": 5, - "duration": 1871, - "created": "2019-05-16T22:10:52+01:00", - "year": 1983, + "name": "album-1", + "songCount": 3, + "duration": 300, + "year": 2021, "song": [ { - "id": "tr-1", - "album": "Snake Charmer", + "id": "tr-4", + "album": "album-1", "albumId": "al-3", - "artist": "Jah Wobble, The Edge & Holger Czukay", + "artist": "artist-0", "artistId": "ar-1", - "bitRate": 882, + "bitRate": 100, "contentType": "audio/x-flac", "coverArt": "al-3", - "created": "2019-07-08T21:49:40.978045401+01:00", - "duration": 372, + "created": "2019-11-30T00:00:00Z", + "duration": 100, "isDir": false, "isVideo": false, "parent": "al-3", - "path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/01.05 Snake Charmer.flac", - "size": 41274185, + "path": "artist-0/album-1/track-0.flac", "suffix": "flac", - "title": "Snake Charmer", + "title": "title-0", "track": 1, "discNumber": 1, "type": "music", - "year": 1983 - }, - { - "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 + "year": 2021 }, { "id": "tr-5", - "album": "Snake Charmer", + "album": "album-1", "albumId": "al-3", - "artist": "Jah Wobble, The Edge & Holger Czukay", + "artist": "artist-0", "artistId": "ar-1", - "bitRate": 976, + "bitRate": 100, "contentType": "audio/x-flac", "coverArt": "al-3", - "created": "2019-07-08T21:49:40.984853203+01:00", - "duration": 227, + "created": "2019-11-30T00:00:00Z", + "duration": 100, "isDir": false, "isVideo": false, "parent": "al-3", - "path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/04.05 Sleazy.flac", - "size": 27938750, + "path": "artist-0/album-1/track-1.flac", "suffix": "flac", - "title": "Sleazy", - "track": 4, + "title": "title-1", + "track": 1, "discNumber": 1, "type": "music", - "year": 1983 + "year": 2021 }, { - "id": "tr-4", - "album": "Snake Charmer", + "id": "tr-6", + "album": "album-1", "albumId": "al-3", - "artist": "Jah Wobble, The Edge & Holger Czukay", + "artist": "artist-0", "artistId": "ar-1", - "bitRate": 884, + "bitRate": 100, "contentType": "audio/x-flac", "coverArt": "al-3", - "created": "2019-07-08T21:49:40.983301328+01:00", - "duration": 418, + "created": "2019-11-30T00:00:00Z", + "duration": 100, "isDir": false, "isVideo": false, "parent": "al-3", - "path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/05.05 Snake Charmer (reprise).flac", - "size": 46427922, + "path": "artist-0/album-1/track-2.flac", "suffix": "flac", - "title": "Snake Charmer (reprise)", - "track": 5, + "title": "title-2", + "track": 1, "discNumber": 1, "type": "music", - "year": 1983 + "year": 2021 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_album_without_cover b/server/ctrlsubsonic/testdata/test_get_album_without_cover index 46aa4bd..80c6353 100644 --- a/server/ctrlsubsonic/testdata/test_get_album_without_cover +++ b/server/ctrlsubsonic/testdata/test_get_album_without_cover @@ -5,12 +5,84 @@ "type": "gonic", "album": { "id": "al-2", + "coverArt": "al-2", + "artistId": "ar-1", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "", - "songCount": 0, - "duration": 0, - "created": "2019-05-16T22:10:21+01:00" + "name": "album-0", + "songCount": 3, + "duration": 300, + "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 + } + ] } } } diff --git a/server/ctrlsubsonic/testdata/test_get_artist_id_one b/server/ctrlsubsonic/testdata/test_get_artist_id_one index 2697244..320d5fe 100644 --- a/server/ctrlsubsonic/testdata/test_get_artist_id_one +++ b/server/ctrlsubsonic/testdata/test_get_artist_id_one @@ -5,21 +5,47 @@ "type": "gonic", "artist": { "id": "ar-1", - "name": "Jah Wobble, The Edge & Holger Czukay", - "albumCount": 1, + "name": "artist-0", + "albumCount": 3, "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", "coverArt": "al-3", "artistId": "ar-1", - "artist": "Jah Wobble, The Edge & Holger Czukay", + "artist": "artist-0", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Snake Charmer", - "songCount": 5, - "duration": 1871, - "created": "2019-05-16T22:10:52+01:00", - "year": 1983 + "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 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_artist_id_three b/server/ctrlsubsonic/testdata/test_get_artist_id_three index 8d81f2a..b29bc8b 100644 --- a/server/ctrlsubsonic/testdata/test_get_artist_id_three +++ b/server/ctrlsubsonic/testdata/test_get_artist_id_three @@ -5,34 +5,47 @@ "type": "gonic", "artist": { "id": "ar-3", - "name": "13th Floor Elevators", - "albumCount": 2, + "name": "artist-2", + "albumCount": 3, "album": [ { - "id": "al-8", - "coverArt": "al-8", + "id": "al-11", + "coverArt": "al-11", "artistId": "ar-3", - "artist": "13th Floor Elevators", + "artist": "artist-2", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "Easter Everywhere", - "songCount": 10, - "duration": 2609, - "created": "2019-06-13T12:57:28.850090338+01:00", - "year": 1967 + "name": "album-0", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-9", - "coverArt": "al-9", + "id": "al-12", + "coverArt": "al-12", "artistId": "ar-3", - "artist": "13th Floor Elevators", + "artist": "artist-2", + "created": "2019-11-30T00:00:00Z", "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 + "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 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_artist_id_two b/server/ctrlsubsonic/testdata/test_get_artist_id_two index a63a6e5..ac1b598 100644 --- a/server/ctrlsubsonic/testdata/test_get_artist_id_two +++ b/server/ctrlsubsonic/testdata/test_get_artist_id_two @@ -5,33 +5,47 @@ "type": "gonic", "artist": { "id": "ar-2", - "name": "A Certain Ratio", - "albumCount": 2, + "name": "artist-1", + "albumCount": 3, "album": [ { - "id": "al-5", - "coverArt": "al-5", + "id": "al-7", + "coverArt": "al-7", "artistId": "ar-2", - "artist": "A Certain Ratio", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "The Graveyard and the Ballroom", - "songCount": 14, - "duration": 2738, - "created": "2019-06-05T17:46:37.675917974+01:00", - "year": 1994 + "name": "album-0", + "songCount": 3, + "duration": 300, + "year": 2021 }, { - "id": "al-6", + "id": "al-8", + "coverArt": "al-8", "artistId": "ar-2", - "artist": "A Certain Ratio", + "artist": "artist-1", + "created": "2019-11-30T00:00:00Z", "title": "", "album": "", - "name": "To Each...", - "songCount": 9, - "duration": 2801, - "created": "2019-05-23T15:12:02.921473302+01:00", - "year": 1981 + "name": "album-1", + "songCount": 3, + "duration": 300, + "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": 3, + "duration": 300, + "year": 2021 } ] } diff --git a/server/ctrlsubsonic/testdata/test_get_artists_no_args b/server/ctrlsubsonic/testdata/test_get_artists_no_args index 0d31203..5ca1d77 100644 --- a/server/ctrlsubsonic/testdata/test_get_artists_no_args +++ b/server/ctrlsubsonic/testdata/test_get_artists_no_args @@ -6,69 +6,12 @@ "artists": { "ignoredArticles": "", "index": [ - { - "name": "#", - "artist": [ - { - "id": "ar-3", - "name": "13th Floor Elevators", - "albumCount": 2 - } - ] - }, { "name": "a", "artist": [ - { - "id": "ar-2", - "name": "A Certain Ratio", - "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 - } + { "id": "ar-1", "name": "artist-0", "albumCount": 3 }, + { "id": "ar-2", "name": "artist-1", "albumCount": 3 }, + { "id": "ar-3", "name": "artist-2", "albumCount": 3 } ] } ] diff --git a/server/ctrlsubsonic/testdata/test_get_indexes_no_args b/server/ctrlsubsonic/testdata/test_get_indexes_no_args index e82d6ee..04d00df 100644 --- a/server/ctrlsubsonic/testdata/test_get_indexes_no_args +++ b/server/ctrlsubsonic/testdata/test_get_indexes_no_args @@ -7,69 +7,12 @@ "lastModified": 0, "ignoredArticles": "", "index": [ - { - "name": "#", - "artist": [ - { - "id": "al-7", - "name": "13th Floor Lowervators", - "albumCount": 2 - }, - { - "id": "al-10", - "name": "___Anika", - "albumCount": 2 - } - ] - }, { "name": "a", "artist": [ - { - "id": "al-4", - "name": "A Certain Ratio", - "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 - } + { "id": "al-2", "name": "album-0", "albumCount": 0 }, + { "id": "al-3", "name": "album-1", "albumCount": 0 }, + { "id": "al-4", "name": "album-2", "albumCount": 0 } ] } ] diff --git a/server/ctrlsubsonic/testdata/test_get_music_directory_with_tracks b/server/ctrlsubsonic/testdata/test_get_music_directory_with_tracks index 2cc15b9..41f5037 100644 --- a/server/ctrlsubsonic/testdata/test_get_music_directory_with_tracks +++ b/server/ctrlsubsonic/testdata/test_get_music_directory_with_tracks @@ -5,106 +5,62 @@ "type": "gonic", "directory": { "id": "al-3", - "parent": "al-2", - "name": "(1983) Snake Charmer", + "name": "album-1", "child": [ { - "id": "tr-1", - "album": "(1983) Snake Charmer", - "artist": "Jah Wobble, The Edge & Holger Czukay", - "bitRate": 882, + "id": "tr-4", + "album": "album-1", + "artist": "artist-0", + "bitRate": 100, "contentType": "audio/x-flac", "coverArt": "al-3", - "created": "2019-07-08T21:49:40.978045401+01:00", - "duration": 372, + "created": "2019-11-30T00:00:00Z", + "duration": 100, "isDir": false, "isVideo": false, "parent": "al-3", - "path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/01.05 Snake Charmer.flac", - "size": 41274185, + "path": "artist-0/album-1/track-0.flac", "suffix": "flac", - "title": "Snake Charmer", + "title": "title-0", "track": 1, "discNumber": 1, "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", - "album": "(1983) Snake Charmer", - "artist": "Jah Wobble, The Edge & Holger Czukay", - "bitRate": 976, + "album": "album-1", + "artist": "artist-0", + "bitRate": 100, "contentType": "audio/x-flac", "coverArt": "al-3", - "created": "2019-07-08T21:49:40.984853203+01:00", - "duration": 227, + "created": "2019-11-30T00:00:00Z", + "duration": 100, "isDir": false, "isVideo": false, "parent": "al-3", - "path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/04.05 Sleazy.flac", - "size": 27938750, + "path": "artist-0/album-1/track-1.flac", "suffix": "flac", - "title": "Sleazy", - "track": 4, + "title": "title-1", + "track": 1, "discNumber": 1, "type": "music" }, { - "id": "tr-4", - "album": "(1983) Snake Charmer", - "artist": "Jah Wobble, The Edge & Holger Czukay", - "bitRate": 884, + "id": "tr-6", + "album": "album-1", + "artist": "artist-0", + "bitRate": 100, "contentType": "audio/x-flac", "coverArt": "al-3", - "created": "2019-07-08T21:49:40.983301328+01:00", - "duration": 418, + "created": "2019-11-30T00:00:00Z", + "duration": 100, "isDir": false, "isVideo": false, "parent": "al-3", - "path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/05.05 Snake Charmer (reprise).flac", - "size": 46427922, + "path": "artist-0/album-1/track-2.flac", "suffix": "flac", - "title": "Snake Charmer (reprise)", - "track": 5, + "title": "title-2", + "track": 1, "discNumber": 1, "type": "music" } diff --git a/server/ctrlsubsonic/testdata/test_get_music_directory_without_tracks b/server/ctrlsubsonic/testdata/test_get_music_directory_without_tracks index aab19d3..08b52d9 100644 --- a/server/ctrlsubsonic/testdata/test_get_music_directory_without_tracks +++ b/server/ctrlsubsonic/testdata/test_get_music_directory_without_tracks @@ -5,16 +5,64 @@ "type": "gonic", "directory": { "id": "al-2", - "name": "Jah Wobble, The Edge, Holger Czukay", + "name": "album-0", "child": [ { - "id": "al-3", - "coverArt": "al-3", - "created": "2019-05-16T22:10:52+01:00", - "isDir": true, + "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", - "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" } ] } diff --git a/server/ctrlsubsonic/testdata/test_search_three_q_13 b/server/ctrlsubsonic/testdata/test_search_three_q_13 deleted file mode 100644 index 99f9933..0000000 --- a/server/ctrlsubsonic/testdata/test_search_three_q_13 +++ /dev/null @@ -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 - } - ] - } - } -} diff --git a/server/ctrlsubsonic/testdata/test_search_three_q_alb b/server/ctrlsubsonic/testdata/test_search_three_q_alb new file mode 100644 index 0000000..ebf616b --- /dev/null +++ b/server/ctrlsubsonic/testdata/test_search_three_q_alb @@ -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 + } + ] + } + } +} diff --git a/server/ctrlsubsonic/testdata/test_search_three_q_ani b/server/ctrlsubsonic/testdata/test_search_three_q_ani deleted file mode 100644 index 7535719..0000000 --- a/server/ctrlsubsonic/testdata/test_search_three_q_ani +++ /dev/null @@ -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 - } - ] - } - } -} diff --git a/server/ctrlsubsonic/testdata/test_search_three_q_art b/server/ctrlsubsonic/testdata/test_search_three_q_art new file mode 100644 index 0000000..d744675 --- /dev/null +++ b/server/ctrlsubsonic/testdata/test_search_three_q_art @@ -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 } + ] + } + } +} diff --git a/server/ctrlsubsonic/testdata/test_search_three_q_cert b/server/ctrlsubsonic/testdata/test_search_three_q_cert deleted file mode 100644 index 712d992..0000000 --- a/server/ctrlsubsonic/testdata/test_search_three_q_cert +++ /dev/null @@ -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 - } - ] - } - } -} diff --git a/server/ctrlsubsonic/testdata/test_search_three_q_tra b/server/ctrlsubsonic/testdata/test_search_three_q_tra new file mode 100644 index 0000000..d654fa5 --- /dev/null +++ b/server/ctrlsubsonic/testdata/test_search_three_q_tra @@ -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 + } + ] + } + } +} diff --git a/server/ctrlsubsonic/testdata/test_search_two_q_13 b/server/ctrlsubsonic/testdata/test_search_two_q_13 deleted file mode 100644 index 0ebc515..0000000 --- a/server/ctrlsubsonic/testdata/test_search_two_q_13 +++ /dev/null @@ -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" - } - ] - } - } -} diff --git a/server/ctrlsubsonic/testdata/test_search_two_q_alb b/server/ctrlsubsonic/testdata/test_search_two_q_alb new file mode 100644 index 0000000..00d79cd --- /dev/null +++ b/server/ctrlsubsonic/testdata/test_search_two_q_alb @@ -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" + } + ] + } + } +} diff --git a/server/ctrlsubsonic/testdata/test_search_two_q_ani b/server/ctrlsubsonic/testdata/test_search_two_q_ani deleted file mode 100644 index e84a394..0000000 --- a/server/ctrlsubsonic/testdata/test_search_two_q_ani +++ /dev/null @@ -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" - } - ] - } - } -} diff --git a/server/ctrlsubsonic/testdata/test_search_two_q_art b/server/ctrlsubsonic/testdata/test_search_two_q_art new file mode 100644 index 0000000..ff28680 --- /dev/null +++ b/server/ctrlsubsonic/testdata/test_search_two_q_art @@ -0,0 +1,8 @@ +{ + "subsonic-response": { + "status": "ok", + "version": "1.15.0", + "type": "gonic", + "searchResult2": {} + } +} diff --git a/server/ctrlsubsonic/testdata/test_search_two_q_cert b/server/ctrlsubsonic/testdata/test_search_two_q_cert deleted file mode 100644 index 5e6e643..0000000 --- a/server/ctrlsubsonic/testdata/test_search_two_q_cert +++ /dev/null @@ -1,15 +0,0 @@ -{ - "subsonic-response": { - "status": "ok", - "version": "1.15.0", - "type": "gonic", - "searchResult2": { - "artist": [ - { - "id": "al-4", - "name": "A Certain Ratio" - } - ] - } - } -} diff --git a/server/ctrlsubsonic/testdata/test_search_two_q_tra b/server/ctrlsubsonic/testdata/test_search_two_q_tra new file mode 100644 index 0000000..09544d0 --- /dev/null +++ b/server/ctrlsubsonic/testdata/test_search_two_q_tra @@ -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" + } + ] + } + } +} diff --git a/server/db/db.go b/server/db/db.go index f35f2d4..390ce3d 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -1,39 +1,19 @@ package db import ( + "errors" "fmt" "log" "net/url" "os" "strings" - "github.com/gorilla/securecookie" "github.com/jinzhu/gorm" "gopkg.in/gormigrate.v1" ) -// wrapMigrations wraps a list of migrations to add logging and transactions -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 { +func DefaultOptions() url.Values { return url.Values{ // with this, multiple connections share a single data and schema cache. // 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 { *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 url := url.URL{ Scheme: "file", Opaque: path, } - url.RawQuery = defaultOptions().Encode() + url.RawQuery = options.Encode() db, err := gorm.Open("sqlite3", url.String()) if err != nil { return nil, fmt.Errorf("with gorm: %w", err) @@ -91,34 +77,29 @@ func New(path string) (*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{} - db. - Where("key=?", key). - First(setting) - return setting.Value + if err := db.Where("key=?", key).First(setting).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return "", err + } + return setting.Value, nil } -func (db *DB) SetSetting(key, value string) { - db. +func (db *DB) SetSetting(key, value string) error { + return db. Where(Setting{Key: key}). Assign(Setting{Value: value}). - FirstOrCreate(&Setting{}) -} - -func (db *DB) GetOrCreateKey(key string) string { - value := db.GetSetting(key) - if value == "" { - value = string(securecookie.GenerateRandomKey(32)) - db.SetSetting(key, value) - } - return value + FirstOrCreate(&Setting{}). + Error } func (db *DB) InsertBulkLeftMany(table string, head []string, left int, col []int) error { + if len(col) == 0 { + return nil + } var rows []string var values []interface{} for _, c := range col { @@ -139,7 +120,7 @@ func (db *DB) GetUserByID(id int) *User { Where("id=?", id). First(user). Error - if gorm.IsRecordNotFoundError(err) { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil } return user @@ -151,7 +132,7 @@ func (db *DB) GetUserByName(name string) *User { Where("name=?", name). First(user). Error - if gorm.IsRecordNotFoundError(err) { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil } return user @@ -164,6 +145,9 @@ func (db *DB) Begin() *DB { type ChunkFunc func(*gorm.DB, []int64) error func (db *DB) TransactionChunked(data []int64, cb ChunkFunc) error { + if len(data) == 0 { + return nil + } // https://sqlite.org/limits.html const size = 999 return db.Transaction(func(tx *gorm.DB) error { diff --git a/server/db/db_test.go b/server/db/db_test.go index 8dd4a42..e3540ac 100644 --- a/server/db/db_test.go +++ b/server/db/db_test.go @@ -1,16 +1,16 @@ package db import ( + "io/ioutil" "log" "math/rand" "os" "testing" _ "github.com/jinzhu/gorm/dialects/sqlite" + "github.com/matryer/is" ) -var testDB *DB - func randKey() string { letters := []rune("abcdef0123456789") b := make([]rune, 16) @@ -22,27 +22,31 @@ func randKey() string { func TestGetSetting(t *testing.T) { key := randKey() - // new key - expected := "hello" - testDB.SetSetting(key, expected) - actual := testDB.GetSetting(key) - if actual != expected { - t.Errorf("expected %q, got %q", expected, actual) + value := "howdy" + + is := is.New(t) + + testDB, err := NewMock() + if err != nil { + t.Fatalf("error creating db: %v", err) } - // existing key - expected = "howdy" - testDB.SetSetting(key, expected) - actual = testDB.GetSetting(key) - if actual != expected { - t.Errorf("expected %q, got %q", expected, actual) + if err := testDB.Migrate(MigrationContext{}); err != nil { + t.Fatalf("error migrating db: %v", err) } + + 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) { - var err error - testDB, err = NewMock() - if err != nil { - log.Fatalf("error opening database: %v\n", err) - } + log.SetOutput(ioutil.Discard) os.Exit(m.Run()) } diff --git a/server/db/migrations.go b/server/db/migrations.go index 624a598..8ec12f5 100644 --- a/server/db/migrations.go +++ b/server/db/migrations.go @@ -1,6 +1,7 @@ package db import ( + "errors" "fmt" "github.com/jinzhu/gorm" @@ -31,20 +32,14 @@ func migrateInitSchema() gormigrate.Migration { } } -func migrateCreateInitUser() gormigrate.Migration { - return gormigrate.Migration{ - ID: "202002192019", - Migrate: func(tx *gorm.DB) error { - const ( - initUsername = "admin" - initPassword = "admin" - ) - err := tx. - Where("name=?", initUsername). - First(&User{}). - Error - if !gorm.IsRecordNotFoundError(err) { - return nil +func construct(ctx MigrationContext, id string, f func(*gorm.DB, MigrationContext) error) *gormigrate.Migration { + return &gormigrate.Migration{ + ID: id, + Migrate: func(db *gorm.DB) error { + tx := db.Begin() + defer tx.Commit() + if err := f(tx, ctx); err != nil { + return fmt.Errorf("%q: %w", id, err) } return tx.Create(&User{ diff --git a/server/db/model.go b/server/db/model.go index 8d2e5b4..f6065ca 100644 --- a/server/db/model.go +++ b/server/db/model.go @@ -88,7 +88,7 @@ type Track struct { Artist *Artist ArtistID int `gorm:"not null" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` Genres []*Genre `gorm:"many2many:track_genres"` - Size int `gorm:"not null" sql:"default: null"` + Size int `sql:"default: null"` Length int `sql:"default: null"` Bitrate int `sql:"default: null"` TagTitle string `sql:"default: null"` diff --git a/server/mockfs/mockfs.go b/server/mockfs/mockfs.go new file mode 100644 index 0000000..093fb47 --- /dev/null +++ b/server/mockfs/mockfs.go @@ -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) diff --git a/server/podcasts/podcasts.go b/server/podcasts/podcasts.go index fb9b914..1aa90cc 100644 --- a/server/podcasts/podcasts.go +++ b/server/podcasts/podcasts.go @@ -24,16 +24,25 @@ import ( "go.senan.xyz/gonic/server/scanner/tags" ) -const DownloadAllWaitInterval = 3 * time.Second +const downloadAllWaitInterval = 3 * time.Second type Podcasts struct { - DB *db.DB - PodcastBasePath string + db *db.DB + 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) { podcasts := []*db.Podcast{} - q := p.DB.Where("user_id=?", userID) + q := p.db.Where("user_id=?", userID) if id != 0 { 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) { episodes := []*db.PodcastEpisode{} - err := p.DB. + err := p.db. Where("podcast_id=?", podcastID). Order("publish_date DESC"). Find(&episodes). @@ -75,12 +84,12 @@ func (p *Podcasts) AddNewPodcast(rssURL string, feed *gofeed.Feed, Title: feed.Title, URL: rssURL, } - podPath := podcast.Fullpath(p.PodcastBasePath) + podPath := podcast.Fullpath(p.baseDir) err := os.Mkdir(podPath, 0755) if err != nil && !os.IsExist(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 } 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 { podcast := db.Podcast{} - err := p.DB. + err := p.db. Where("id=?", podcastID). First(&podcast). Error @@ -104,7 +113,7 @@ func (p *Podcasts) SetAutoDownload(podcastID int, setting db.PodcastAutoDownload return err } 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 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 { podcastEpisode := db.PodcastEpisode{} - err := p.DB. + err := p.db. Where("podcast_id=?", podcast.ID). Order("publish_date DESC"). 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 err := p.DB.Save(episode).Error; err != nil { + if err := p.db.Save(episode).Error; err != nil { return nil, err } return episode, nil } 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 episode, nil @@ -259,7 +268,7 @@ func (p *Podcasts) findMediaAudio(podcastID, duration int, func (p *Podcasts) RefreshPodcasts() error { 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) } var errs *multierr.Err @@ -271,7 +280,7 @@ func (p *Podcasts) RefreshPodcasts() error { func (p *Podcasts) RefreshPodcastsForUser(userID int) error { podcasts := []*db.Podcast{} - err := p.DB. + err := p.db. Where("user_id=?", userID). Find(&podcasts). Error @@ -304,7 +313,7 @@ func (p *Podcasts) refreshPodcasts(podcasts []*db.Podcast) error { func (p *Podcasts) DownloadPodcastAll(podcastID int) error { podcastEpisodes := []db.PodcastEpisode{} - err := p.DB. + err := p.db. Where("podcast_id=?", podcastID). Find(&podcastEpisodes). Error @@ -322,7 +331,7 @@ func (p *Podcasts) DownloadPodcastAll(podcastID int) error { continue } log.Printf("finished downloading episode: %q", episode.Title) - time.Sleep(DownloadAllWaitInterval) + time.Sleep(downloadAllWaitInterval) } }() return nil @@ -331,14 +340,14 @@ func (p *Podcasts) DownloadPodcastAll(podcastID int) error { func (p *Podcasts) DownloadEpisode(episodeID int) error { podcastEpisode := db.PodcastEpisode{} podcast := db.Podcast{} - err := p.DB. + err := p.db. Where("id=?", episodeID). First(&podcastEpisode). Error if err != nil { return fmt.Errorf("get podcast episode by id: %w", err) } - err = p.DB. + err = p.db. Where("id=?", podcastEpisode.PodcastID). First(&podcast). Error @@ -350,7 +359,7 @@ func (p *Podcasts) DownloadEpisode(episodeID int) error { return nil } podcastEpisode.Status = db.PodcastEpisodeStatusDownloading - p.DB.Save(&podcastEpisode) + p.db.Save(&podcastEpisode) // nolint: bodyclose resp, err := http.Get(podcastEpisode.AudioURL) if err != nil { @@ -365,14 +374,14 @@ func (p *Podcasts) DownloadEpisode(episodeID int) error { filename = path.Base(audioURL.Path) } 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 { return fmt.Errorf("create audio file: %w", err) } podcastEpisode.Filename = filename sanTitle := strings.ReplaceAll(podcast.Title, "/", "_") podcastEpisode.Path = path.Join(sanTitle, filename) - p.DB.Save(&podcastEpisode) + p.db.Save(&podcastEpisode) go func() { if err := p.doPodcastDownload(&podcastEpisode, audioFile, resp.Body); err != nil { log.Printf("error downloading podcast: %v", err) @@ -385,18 +394,18 @@ func (p *Podcasts) findUniqueEpisodeName( podcast *db.Podcast, podcastEpisode *db.PodcastEpisode, 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) { return filename } sanitizedTitle := strings.ReplaceAll(podcastEpisode.Title, "/", "_") 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) { return titlePath } // 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 { @@ -442,7 +451,7 @@ func (p *Podcasts) downloadPodcastCover(podPath string, podcast *db.Podcast) err podcastPath := filepath.Clean(strings.ReplaceAll(podcast.Title, "/", "_")) podcastFilename := fmt.Sprintf("cover%s", ext) 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 nil @@ -454,24 +463,24 @@ func (p *Podcasts) doPodcastDownload(podcastEpisode *db.PodcastEpisode, file *os } defer file.Close() stat, _ := file.Stat() - podcastPath := path.Join(p.PodcastBasePath, podcastEpisode.Path) - podcastTags, err := tags.New(podcastPath) + podcastPath := path.Join(p.baseDir, podcastEpisode.Path) + podcastTags, err := p.tagger.Read(podcastPath) if err != nil { log.Printf("error parsing podcast audio: %e", err) podcastEpisode.Status = db.PodcastEpisodeStatusError - p.DB.Save(podcastEpisode) + p.db.Save(podcastEpisode) return nil } podcastEpisode.Bitrate = podcastTags.Bitrate() podcastEpisode.Status = db.PodcastEpisodeStatusCompleted podcastEpisode.Length = podcastTags.Length() 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 { podcast := db.Podcast{} - err := p.DB. + err := p.db. Where("id=? AND user_id=?", podcastID, userID). First(&podcast). Error @@ -479,17 +488,17 @@ func (p *Podcasts) DeletePodcast(userID, podcastID int) error { return err } var userCount int - p.DB. + p.db. Model(&db.Podcast{}). Where("title=?", podcast.Title). Count(&userCount) if userCount == 1 { // 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) } } - err = p.DB. + err = p.db. Where("id=? AND user_id=?", podcastID, userID). Delete(db.Podcast{}). Error @@ -501,13 +510,13 @@ func (p *Podcasts) DeletePodcast(userID, podcastID int) error { func (p *Podcasts) DeletePodcastEpisode(podcastEpisodeID int) error { episode := db.PodcastEpisode{} - err := p.DB.First(&episode, podcastEpisodeID).Error + err := p.db.First(&episode, podcastEpisodeID).Error if err != nil { return err } episode.Status = db.PodcastEpisodeStatusDeleted - p.DB.Save(&episode) - if err := os.Remove(filepath.Join(p.PodcastBasePath, episode.Path)); err != nil { + p.db.Save(&episode) + if err := os.Remove(filepath.Join(p.baseDir, episode.Path)); err != nil { return err } return err diff --git a/server/scanner/scanner.go b/server/scanner/scanner.go index c966305..5370596 100644 --- a/server/scanner/scanner.go +++ b/server/scanner/scanner.go @@ -5,8 +5,8 @@ import ( "fmt" "log" "os" - "path" "path/filepath" + "sort" "strconv" "strings" "sync/atomic" @@ -16,9 +16,9 @@ import ( "github.com/karrick/godirwalk" "github.com/rainycape/unidecode" + "go.senan.xyz/gonic/multierr" "go.senan.xyz/gonic/server/db" "go.senan.xyz/gonic/server/mime" - "go.senan.xyz/gonic/server/scanner/stack" "go.senan.xyz/gonic/server/scanner/tags" ) @@ -28,70 +28,347 @@ var ( 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 { db *db.DB - musicPath string - isFull bool + musicPaths []string + sorted bool genreSplit string - // these two are for the transaction we do for every album. - // the boolean is there so we dont begin or commit multiple - // 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 + tagger tags.Reader + scanning *int32 } -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{ db: db, - musicPath: musicPath, + musicPaths: musicPaths, + sorted: sorted, genreSplit: genreSplit, + tagger: tagger, + scanning: new(int32), } } -// ## begin clean funcs -// ## begin clean funcs -// ## begin clean funcs +type ScanOptions struct { + IsFull bool + // 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() var previous []int var missing []int64 @@ -103,7 +380,7 @@ func (s *Scanner) cleanTracks() error { return fmt.Errorf("plucking ids: %w", err) } for _, prev := range previous { - if _, ok := s.seenTracks[prev]; !ok { + if _, ok := seenTracks[prev]; !ok { missing = append(missing, int64(prev)) } } @@ -117,7 +394,7 @@ func (s *Scanner) cleanTracks() error { return nil } -func (s *Scanner) cleanAlbums() error { +func (s *Scanner) cleanAlbums(seenAlbums map[int]struct{}) error { start := time.Now() var previous []int var missing []int64 @@ -129,7 +406,7 @@ func (s *Scanner) cleanAlbums() error { return fmt.Errorf("plucking ids: %w", err) } for _, prev := range previous { - if _, ok := s.seenAlbums[prev]; !ok { + if _, ok := seenAlbums[prev]; !ok { missing = append(missing, int64(prev)) } } @@ -176,383 +453,50 @@ func (s *Scanner) cleanGenres() error { Where("album_genres.genre_id IS NULL"). SubQuery() q := s.db. - Where("genres.id IN ?", subTrack). - Or("genres.id IN ?", subAlbum). + Where("genres.id IN ? AND genres.id IN ?", subTrack, subAlbum). Delete(&db.Genre{}) log.Printf("finished clean genres in %s, %d removed", durSince(start), q.RowsAffected) return nil } -// ## begin entries -// ## begin entries -// ## begin entries - -type ScanOptions struct { - IsFull bool - // TODO https://github.com/sentriz/gonic/issues/64 - Path string +func ext(name string) string { + ext := filepath.Ext(name) + if len(ext) == 0 { + return "" + } + return ext[1:] } -func (s *Scanner) Start(opts ScanOptions) error { - if IsScanning() { - return ErrAlreadyScanning - } - unSet := SetScanning() - defer unSet() - - // reset state vars for the new scan - s.isFull = opts.IsFull - s.seenTracks = map[int]struct{}{} - 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 { +func isCover(name string) bool { + switch path := strings.ToLower(name); path { + case + "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": + return true + default: return false } - return statModTime.Before(updatedInDB) } -func (s *Scanner) handleAlbum(it *item) error { - if s.trTxOpen { - // a transaction still being open when we handle an album can - // happen if there is a album that contains /both/ tracks and - // sub albums - s.trTx.Commit() - s.trTxOpen = false +// 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 } - album := &db.Album{} - 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 + return "" } -func (s *Scanner) handleTrack(it *item) error { - if !s.trTxOpen { - 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 durSince(t time.Time) time.Duration { + return time.Since(t).Truncate(10 * time.Microsecond) } -func (s *Scanner) populateAlbumArtist(trags *tags.Tags) (*db.Artist, error) { - var artist db.Artist - artistName := trags.SomeAlbumArtist() - err := s.trTx. - 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 +type collected struct { + seenTracks map[int]struct{} + seenAlbums map[int]struct{} + seenTracksNew int } diff --git a/server/scanner/scanner_test.go b/server/scanner/scanner_test.go index 64d90af..6cac472 100644 --- a/server/scanner/scanner_test.go +++ b/server/scanner/scanner_test.go @@ -1,4 +1,4 @@ -package scanner +package scanner_test import ( "io/ioutil" @@ -6,62 +6,319 @@ import ( "os" "testing" + "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" + "github.com/matryer/is" "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) { - 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) os.Exit(m.Run()) } -// RESULTS fresh -// 20 times / 1.436 -// 20 times / 1.39 +func TestTableCounts(t *testing.T) { + t.Parallel() + is := is.NewRelaxed(t) + m := mockfs.New(t) + defer m.CleanUp() -// RESULTS inc -// 100 times / 1.86 -// 100 times / 1.9 -// 100 times / 1.5 -// 100 times / 1.48 + m.AddItems() + m.ScanAndClean() + + var tracks int + 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? +} diff --git a/server/scanner/stack/stack.go b/server/scanner/stack/stack.go deleted file mode 100644 index 937b78f..0000000 --- a/server/scanner/stack/stack.go +++ /dev/null @@ -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() -} diff --git a/server/scanner/stack/stack_test.go b/server/scanner/stack/stack_test.go deleted file mode 100644 index e838971..0000000 --- a/server/scanner/stack/stack_test.go +++ /dev/null @@ -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) - } -} diff --git a/server/scanner/tags/tags.go b/server/scanner/tags/tags.go index 9e0f1d3..58009b0 100644 --- a/server/scanner/tags/tags.go +++ b/server/scanner/tags/tags.go @@ -7,6 +7,56 @@ import ( "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 { if in == "" { return 0 @@ -19,48 +69,26 @@ func intSep(in, sep string) int { return out } -type Tags struct { - raw map[string]string - props *audiotags.AudioProperties +type Reader interface { + Read(abspath string) (Parser, error) } -func New(path string) (*Tags, error) { - raw, props, err := audiotags.Read(path) - return &Tags{raw, props}, err -} - -func (t *Tags) firstTag(keys ...string) string { - for _, key := range keys { - if val, ok := t.raw[key]; ok { - return val - } - } - return "" -} - -func (t *Tags) Title() string { return t.firstTag("title") } -func (t *Tags) BrainzID() string { return t.firstTag("musicbrainz_trackid") } -func (t *Tags) Artist() string { return t.firstTag("artist") } -func (t *Tags) Album() string { return t.firstTag("album") } -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 +type Parser interface { + Title() string + BrainzID() string + Artist() string + Album() string + AlbumArtist() string + AlbumBrainzID() string + Genre() string + TrackNumber() int + DiscNumber() int + Length() int + Bitrate() int + Year() int + + SomeAlbum() string + SomeArtist() string + SomeAlbumArtist() string + SomeGenre() string } diff --git a/server/scrobble/lastfm/lastfm.go b/server/scrobble/lastfm/lastfm.go index 851929f..2b66608 100644 --- a/server/scrobble/lastfm/lastfm.go +++ b/server/scrobble/lastfm/lastfm.go @@ -146,8 +146,15 @@ func (s *Scrobbler) Scrobble(user *db.User, track *db.Track, stamp time.Time, su if user.LastFMSession == "" { return nil } - apiKey := s.DB.GetSetting("lastfm_api_key") - secret := s.DB.GetSetting("lastfm_secret") + apiKey, err := s.DB.GetSetting("lastfm_api_key") + 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 if user.LastFMSession == "" { 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("albumArtist", track.Artist.Name) params.Add("api_sig", getParamSignature(params, secret)) - _, err := makeRequest("POST", params) + _, err = makeRequest("POST", params) return err } diff --git a/server/server.go b/server/server.go index 3b0c03b..6e2badf 100644 --- a/server/server.go +++ b/server/server.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gorilla/mux" + "github.com/gorilla/securecookie" "github.com/wader/gormstore" "go.senan.xyz/gonic/server/assets" @@ -18,6 +19,7 @@ import ( "go.senan.xyz/gonic/server/jukebox" "go.senan.xyz/gonic/server/podcasts" "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/lastfm" "go.senan.xyz/gonic/server/scrobble/listenbrainz" @@ -48,7 +50,9 @@ func New(opts Options) (*Server, error) { opts.CachePath = filepath.Clean(opts.CachePath) 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{ DB: opts.DB, MusicPath: opts.MusicPath, @@ -63,12 +67,21 @@ func New(opts Options) (*Server, error) { } 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.SessionOpts.HttpOnly = true 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) if err != nil { @@ -78,11 +91,10 @@ func New(opts Options) (*Server, error) { Controller: base, CachePath: opts.CachePath, CoverCachePath: opts.CoverCachePath, - Scrobblers: []scrobble.Scrobbler{ - &lastfm.Scrobbler{DB: opts.DB}, - &listenbrainz.Scrobbler{}, - }, - Podcasts: podcast, + PodcastsPath: opts.PodcastPath, + Jukebox: &jukebox.Jukebox{}, + Scrobblers: []scrobble.Scrobbler{&lastfm.Scrobbler{DB: opts.DB}, &listenbrainz.Scrobbler{}}, + Podcasts: podcast, } setupMisc(r, base) @@ -272,7 +284,7 @@ func (s *Server) StartScanTicker(dur time.Duration) (FuncExecute, FuncInterrupt) return nil case <-ticker.C: 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) } }()