mirror of
https://github.com/navidrome/navidrome.git
synced 2026-01-03 12:28:05 -05:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9f5625abf | ||
|
|
22d57a7c26 | ||
|
|
0c5bf18d80 | ||
|
|
9b7d1757e7 | ||
|
|
c34a5dcb07 | ||
|
|
90a1e6d213 | ||
|
|
482350c076 | ||
|
|
64388b2d4a | ||
|
|
3007ca68d5 | ||
|
|
d4edff3aaa | ||
|
|
99b1dc1421 | ||
|
|
37dfe4c092 | ||
|
|
1278863416 | ||
|
|
cffae3a7d6 | ||
|
|
0d2911daf9 | ||
|
|
3c54b776d6 | ||
|
|
34127805ce | ||
|
|
ac4aa1ebe2 | ||
|
|
b7d7251cf4 | ||
|
|
7bf110f22f | ||
|
|
3b651a3728 | ||
|
|
13390c2edb | ||
|
|
221d320ccf |
@@ -19,7 +19,7 @@ builds:
|
||||
flags:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- -X main.gitSha={{.ShortCommit}} -X main.gitTag={{.Tag}}
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
|
||||
|
||||
- id: navidrome_linux
|
||||
env:
|
||||
@@ -32,7 +32,7 @@ builds:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -X main.gitSha={{.ShortCommit}} -X main.gitTag={{.Tag}}
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
|
||||
|
||||
- id: navidrome_windows_i686
|
||||
env:
|
||||
@@ -47,7 +47,7 @@ builds:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -X main.gitSha={{.ShortCommit}} -X main.gitTag={{.Tag}}
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
|
||||
|
||||
- id: navidrome_windows_x64
|
||||
env:
|
||||
@@ -62,7 +62,7 @@ builds:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -X main.gitSha={{.ShortCommit}} -X main.gitTag={{.Tag}}
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
|
||||
|
||||
archives:
|
||||
-
|
||||
@@ -84,3 +84,5 @@ changelog:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^chore:'
|
||||
- '^ci:'
|
||||
|
||||
@@ -63,7 +63,7 @@ Navidrome is actively being tested with:
|
||||
| `star` | |
|
||||
| `unstar` | |
|
||||
| `setRating` | Doesn't work with artists |
|
||||
| `scrobble` | No Last.FM support yet. It is used to update play count, last played, skip count and last skipped |
|
||||
| `scrobble` | No Last.FM support yet. It is used to update play count and last played |
|
||||
| ||
|
||||
| _USER MANAGEMENT_ ||
|
||||
| `getUser` | Hardcoded all roles, ignores `username` parameter|
|
||||
|
||||
28
Dockerfile
28
Dockerfile
@@ -18,8 +18,7 @@ RUN apk add -U --no-cache build-base git
|
||||
RUN go get -u github.com/go-bindata/go-bindata/...
|
||||
|
||||
# Download and unpack static ffmpeg
|
||||
ARG FFMPEG_VERSION=4.1.4
|
||||
ARG FFMPEG_URL=https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-${FFMPEG_VERSION}-amd64-static.tar.xz
|
||||
ARG FFMPEG_URL=https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
||||
RUN wget -O /tmp/ffmpeg.tar.xz ${FFMPEG_URL}
|
||||
RUN cd /tmp && tar xJf ffmpeg.tar.xz && rm ffmpeg.tar.xz
|
||||
|
||||
@@ -28,28 +27,41 @@ WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source and UI bundle, build executable
|
||||
# Copy source, test it
|
||||
COPY . .
|
||||
RUN go test ./...
|
||||
|
||||
# Copy UI bundle, build executable
|
||||
COPY --from=jsbuilder /src/build/* /src/ui/build/
|
||||
COPY --from=jsbuilder /src/build/static/css/* /src/ui/build/static/css/
|
||||
COPY --from=jsbuilder /src/build/static/js/* /src/ui/build/static/js/
|
||||
RUN rm -rf /src/build/css /src/build/js
|
||||
RUN go-bindata -fs -prefix ui/build -tags embed -nocompress -pkg assets -o assets/embedded_gen.go ui/build/...\
|
||||
&& go build -ldflags="-X main.gitSha=${SOURCE_COMMIT} -X main.gitTag=${SOURCE_BRANCH}" -tags=embed
|
||||
RUN GIT_TAG=$(git name-rev --name-only HEAD) && \
|
||||
GIT_TAG=${GIT_TAG#"tags/"} && \
|
||||
GIT_SHA=$(git rev-parse --short HEAD) && \
|
||||
echo "Building version: ${GIT_TAG} (${GIT_SHA})" && \
|
||||
go-bindata -fs -prefix ui/build -tags embed -nocompress -pkg assets -o assets/embedded_gen.go ui/build/... && \
|
||||
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=${GIT_SHA} -X github.com/deluan/navidrome/consts.gitTag=${GIT_TAG}" -tags=embed
|
||||
|
||||
#####################################################
|
||||
### Build Final Image
|
||||
FROM alpine
|
||||
FROM alpine as release
|
||||
MAINTAINER Deluan Quintao <navidrome@deluan.com>
|
||||
|
||||
COPY --from=gobuilder /src/navidrome /app/
|
||||
COPY --from=gobuilder /tmp/ffmpeg*/ffmpeg /usr/bin/
|
||||
|
||||
# Check if ffmpeg runs properly
|
||||
RUN ffmpeg -buildconf
|
||||
|
||||
VOLUME ["/data", "/music"]
|
||||
ENV ND_MUSICFOLDER /music
|
||||
ENV ND_DATAFOLDER /data
|
||||
ENV ND_SCANINTERVAL 1m
|
||||
ENV ND_LOGLEVEL info
|
||||
ENV ND_PORT 4533
|
||||
EXPOSE 4533
|
||||
|
||||
EXPOSE 4533
|
||||
WORKDIR /app
|
||||
CMD "/app/navidrome"
|
||||
|
||||
ENTRYPOINT "/app/navidrome"
|
||||
|
||||
32
Makefile
32
Makefile
@@ -4,11 +4,11 @@ NODE_VERSION=v13.7.0
|
||||
GIT_SHA=$(shell git rev-parse --short HEAD)
|
||||
|
||||
.PHONY: dev
|
||||
dev: check_env data
|
||||
dev: check_env
|
||||
@goreman -f Procfile.dev -b 4533 start
|
||||
|
||||
.PHONY: server
|
||||
server: check_go_env data
|
||||
server: check_go_env
|
||||
@reflex -d none -c reflex.conf
|
||||
|
||||
.PHONY: watch
|
||||
@@ -26,12 +26,13 @@ testall: check_go_env test
|
||||
|
||||
.PHONY: setup
|
||||
setup: Jamstash-master
|
||||
@which reflex || (echo "Installing Reflex" && GO111MODULE=off go get -u github.com/cespare/reflex)
|
||||
@which goconvey || (echo "Installing GoConvey" && GO111MODULE=off go get -u github.com/smartystreets/goconvey)
|
||||
@which wire || (echo "Installing Wire" && GO111MODULE=off go get -u github.com/google/wire/cmd/wire)
|
||||
@which go-bindata || (echo "Installing BinData" && GO111MODULE=off go get -u github.com/go-bindata/go-bindata/...)
|
||||
@which reflex || (echo "Installing Reflex" && GO111MODULE=off go get -u github.com/cespare/reflex)
|
||||
@which goreman || (echo "Installing Goreman" && GO111MODULE=off go get -u github.com/mattn/goreman)
|
||||
@which ginkgo || (echo "Installing Ginkgo" && GO111MODULE=off go get -u github.com/onsi/ginkgo/ginkgo)
|
||||
@which go-bindata || (echo "Installing BinData" && GO111MODULE=off go get -u github.com/go-bindata/go-bindata/...)
|
||||
@which goose || (echo "Installing Goose" && GO111MODULE=off go get -u github.com/pressly/goose/cmd/goose)
|
||||
go mod download
|
||||
@(cd ./ui && npm ci)
|
||||
|
||||
@@ -57,9 +58,6 @@ check_node_env:
|
||||
@(hash node) || (echo "\nERROR: Node environment not setup properly!\n"; exit 1)
|
||||
@node --version | grep -q $(NODE_VERSION) || (echo "\nERROR: Please check your Node version. Should be $(NODE_VERSION)\n"; exit 1)
|
||||
|
||||
data:
|
||||
mkdir data
|
||||
|
||||
UI_SRC = $(shell find ui/src ui/public -name "*.js")
|
||||
ui/build: $(UI_SRC) $(UI_PUBLIC) ui/package-lock.json
|
||||
@(cd ./ui && npm run build)
|
||||
@@ -69,8 +67,22 @@ assets/embedded_gen.go: ui/build
|
||||
|
||||
.PHONY: build
|
||||
build: check_go_env
|
||||
go build -ldflags="-X main.gitSha=$(GIT_SHA) -X main.gitTag=master"
|
||||
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=master"
|
||||
|
||||
.PHONY: buildall
|
||||
buildall: check_go_env assets/embedded_gen.go
|
||||
go build -ldflags="-X main.gitSha=$(GIT_SHA) -X main.gitTag=master" -tags=embed
|
||||
buildall: check_env assets/embedded_gen.go
|
||||
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=master" -tags=embed
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
@if [[ ! "${V}" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$$ ]]; then echo "Usage: make release V=X.X.X"; exit 1; fi
|
||||
go mod tidy
|
||||
make test
|
||||
@if [ -n "`git status -s`" ]; then echo "\n\nThere are pending changes. Please commit or stash first"; exit 1; fi
|
||||
git tag v${V}
|
||||
git push origin v${V}
|
||||
git push origin master
|
||||
|
||||
.PHONY: dist
|
||||
dist:
|
||||
docker run -it -v $(PWD):/github/workspace -w /github/workspace bepsays/ci-goreleaser:1.13-4 goreleaser release --rm-dist --skip-publish --snapshot
|
||||
|
||||
33
README.md
33
README.md
@@ -1,14 +1,17 @@
|
||||
# Navidrome Music Streamer
|
||||
|
||||
[](https://github.com/deluan/navidrome/actions)
|
||||
[](https://github.com/deluan/navidrome/actions)
|
||||
[](https://github.com/deluan/navidrome/releases)
|
||||
[](https://hub.docker.com/r/deluan/navidrome)
|
||||
|
||||
Navidrome is an open source web-based music collection server and streamer. It gives you freedom to listen to your
|
||||
music collection from any browser or mobile device.
|
||||
|
||||
This is _alpha quality_ software. Expect some changes in the feature set and the way it works.
|
||||
This is a fully functional _alpha quality_ software. Expect some changes in the feature set and the way it works.
|
||||
|
||||
__Any feedback is welcome!__ If you need/want a new feature, find a bug or think of any way to improve Navidrome,
|
||||
please fill a [GitHub issue](https://github.com/deluan/navidrome/issues)
|
||||
please fill a [GitHub issue](https://github.com/deluan/navidrome/issues) or join the chat in our [Discord server](https://discord.gg/xh7j7yF)
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
@@ -32,7 +35,7 @@ on a frequent basis. Some upcoming features planned:
|
||||
- Transcoding/Downsampling on-the-fly
|
||||
- Last.FM integration
|
||||
- Integrated music player
|
||||
- Pre-build binaries for all platforms, including Raspberry Pi
|
||||
- Pre-build binaries for Raspberry Pi
|
||||
- Smart/dynamic playlists (similar to iTunes)
|
||||
- Jukebox mode
|
||||
- Sharing links to albums/songs/playlists
|
||||
@@ -54,7 +57,7 @@ If you have any issues with these binaries, or need a binary for a different pla
|
||||
|
||||
### Docker
|
||||
|
||||
Docker images are available. Example of usage:
|
||||
[Docker images](https://hub.docker.com/r/deluan/navidrome) are available. They include everything needed to run Navidrome. Example of usage:
|
||||
|
||||
```yaml
|
||||
# This is just an example. Customize it to your needs.
|
||||
@@ -77,16 +80,19 @@ services:
|
||||
- "/Users/deluan/Music/iTunes/iTunes Media/Music:/music"
|
||||
```
|
||||
|
||||
### Build it yourself
|
||||
### Build from source
|
||||
|
||||
You will need to install [Go 1.13](https://golang.org/dl/) and [Node 13.7](http://nodejs.org).
|
||||
You'll also need [ffmpeg](https://ffmpeg.org) installed in your system
|
||||
You will need to install [Go 1.13](https://golang.org/dl/) and [Node 13.7.0](http://nodejs.org).
|
||||
You'll also need [ffmpeg](https://ffmpeg.org) installed in your system. The setup is very strict, and
|
||||
the steps bellow only work with these specific versions (enforced in the Makefile)
|
||||
|
||||
After the prerequisites above are installed, build the application with:
|
||||
After the prerequisites above are installed, clone this repository and build the application with:
|
||||
|
||||
```
|
||||
$ make setup
|
||||
$ make buildall
|
||||
```shell script
|
||||
$ git clone https://github.com/deluan/navidrome
|
||||
$ cd navidrome
|
||||
$ make setup # Install tools required for Navidrome's development
|
||||
$ make buildall # Build UI and server, generates a single executable
|
||||
```
|
||||
|
||||
This will generate the `navidrome` binary executable in the project's root folder.
|
||||
@@ -102,6 +108,8 @@ The server should start listening for requests on the default port __4533__
|
||||
After starting Navidrome for the first time, go to http://localhost:4533. It will ask you to create your first admin
|
||||
user.
|
||||
|
||||
For more options, run `navidrome --help`
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p align="center">
|
||||
@@ -114,7 +122,6 @@ user.
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
## Subsonic API Version Compatibility
|
||||
|
||||
Check the up to date [compatibility table](https://github.com/deluan/navidrome/blob/master/API_COMPATIBILITY.md)
|
||||
|
||||
20
banner.go
20
banner.go
@@ -4,33 +4,17 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/deluan/navidrome/consts"
|
||||
"github.com/deluan/navidrome/static"
|
||||
)
|
||||
|
||||
var (
|
||||
// This will be set in build time. If not, version will be set to "dev"
|
||||
gitTag string
|
||||
gitSha string
|
||||
)
|
||||
|
||||
// Formats:
|
||||
// dev
|
||||
// v0.2.0 (5b84188)
|
||||
// master (9ed35cb)
|
||||
func getVersion() string {
|
||||
if gitSha == "" {
|
||||
return "dev"
|
||||
}
|
||||
return fmt.Sprintf("%s (%s)", gitTag, gitSha)
|
||||
}
|
||||
|
||||
func getBanner() string {
|
||||
data, _ := static.Asset("banner.txt")
|
||||
return strings.TrimSuffix(string(data), "\n")
|
||||
}
|
||||
|
||||
func ShowBanner() {
|
||||
version := "Version: " + getVersion()
|
||||
version := "Version: " + consts.Version()
|
||||
padding := strings.Repeat(" ", 52-len(version))
|
||||
fmt.Printf("%s%s%s\n\n", getBanner(), padding, version)
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ func newWithPath(path string, skipFlags ...bool) *multiconfig.DefaultLoader {
|
||||
if strings.HasSuffix(path, "yml") || strings.HasSuffix(path, "yaml") {
|
||||
loaders = append(loaders, &multiconfig.YAMLLoader{Path: path})
|
||||
}
|
||||
} else {
|
||||
println("Skipping config file not found: ", path)
|
||||
}
|
||||
|
||||
e := &multiconfig.EnvironmentLoader{}
|
||||
@@ -84,6 +82,9 @@ func LoadFromFile(confFile string, skipFlags ...bool) {
|
||||
if Server.DbPath == "" {
|
||||
Server.DbPath = filepath.Join(Server.DataFolder, "navidrome.db")
|
||||
}
|
||||
if os.Getenv("PORT") != "" {
|
||||
Server.Port = os.Getenv("PORT")
|
||||
}
|
||||
log.SerLevelString(Server.LogLevel)
|
||||
log.Trace("Loaded configuration", "file", confFile, "config", fmt.Sprintf("%#v", Server))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package consts
|
||||
import "time"
|
||||
|
||||
const (
|
||||
AppName = "navidrome"
|
||||
|
||||
LocalConfigFile = "./navidrome.toml"
|
||||
InitialSetupFlagKey = "InitialSetup"
|
||||
|
||||
@@ -10,7 +12,5 @@ const (
|
||||
JWTIssuer = "ND"
|
||||
JWTTokenExpiration = 30 * time.Minute
|
||||
|
||||
InitialUserName = "admin"
|
||||
|
||||
UIAssetsLocalPath = "ui/build"
|
||||
)
|
||||
|
||||
20
consts/version.go
Normal file
20
consts/version.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package consts
|
||||
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
// This will be set in build time. If not, version will be set to "dev"
|
||||
gitTag string
|
||||
gitSha string
|
||||
)
|
||||
|
||||
// Formats:
|
||||
// dev
|
||||
// v0.2.0 (5b84188)
|
||||
// master (9ed35cb)
|
||||
func Version() string {
|
||||
if gitSha == "" {
|
||||
return "dev"
|
||||
}
|
||||
return fmt.Sprintf("%s (%s)", gitTag, gitSha)
|
||||
}
|
||||
@@ -32,11 +32,11 @@ type browser struct {
|
||||
}
|
||||
|
||||
func (b *browser) MediaFolders(ctx context.Context) (model.MediaFolders, error) {
|
||||
return b.ds.MediaFolder().GetAll()
|
||||
return b.ds.MediaFolder(ctx).GetAll()
|
||||
}
|
||||
|
||||
func (b *browser) Indexes(ctx context.Context, ifModifiedSince time.Time) (model.ArtistIndexes, time.Time, error) {
|
||||
l, err := b.ds.Property().DefaultGet(model.PropLastScan, "-1")
|
||||
l, err := b.ds.Property(ctx).DefaultGet(model.PropLastScan, "-1")
|
||||
ms, _ := strconv.ParseInt(l, 10, 64)
|
||||
lastModified := utils.ToTime(ms)
|
||||
|
||||
@@ -45,7 +45,7 @@ func (b *browser) Indexes(ctx context.Context, ifModifiedSince time.Time) (model
|
||||
}
|
||||
|
||||
if lastModified.After(ifModifiedSince) {
|
||||
indexes, err := b.ds.Artist().GetIndex()
|
||||
indexes, err := b.ds.Artist(ctx).GetIndex()
|
||||
return indexes, lastModified, err
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ type DirectoryInfo struct {
|
||||
}
|
||||
|
||||
func (b *browser) Artist(ctx context.Context, id string) (*DirectoryInfo, error) {
|
||||
a, albums, err := b.retrieveArtist(id)
|
||||
a, albums, err := b.retrieveArtist(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -81,12 +81,12 @@ func (b *browser) Artist(ctx context.Context, id string) (*DirectoryInfo, error)
|
||||
for _, al := range albums {
|
||||
albumIds = append(albumIds, al.ID)
|
||||
}
|
||||
annMap, err := b.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
annMap, err := b.ds.Annotation(ctx).GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
return b.buildArtistDir(a, albums, annMap), nil
|
||||
}
|
||||
|
||||
func (b *browser) Album(ctx context.Context, id string) (*DirectoryInfo, error) {
|
||||
al, tracks, err := b.retrieveAlbum(id)
|
||||
al, tracks, err := b.retrieveAlbum(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -97,11 +97,11 @@ func (b *browser) Album(ctx context.Context, id string) (*DirectoryInfo, error)
|
||||
}
|
||||
|
||||
userID := getUserID(ctx)
|
||||
trackAnnMap, err := b.ds.Annotation().GetMap(userID, model.MediaItemType, mfIds)
|
||||
trackAnnMap, err := b.ds.Annotation(ctx).GetMap(userID, model.MediaItemType, mfIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ann, err := b.ds.Annotation().Get(userID, model.AlbumItemType, al.ID)
|
||||
ann, err := b.ds.Annotation(ctx).Get(userID, model.AlbumItemType, al.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -121,13 +121,13 @@ func (b *browser) Directory(ctx context.Context, id string) (*DirectoryInfo, err
|
||||
}
|
||||
|
||||
func (b *browser) GetSong(ctx context.Context, id string) (*Entry, error) {
|
||||
mf, err := b.ds.MediaFile().Get(id)
|
||||
mf, err := b.ds.MediaFile(ctx).Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userId := getUserID(ctx)
|
||||
ann, err := b.ds.Annotation().Get(userId, model.MediaItemType, id)
|
||||
ann, err := b.ds.Annotation(ctx).Get(userId, model.MediaItemType, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func (b *browser) GetSong(ctx context.Context, id string) (*Entry, error) {
|
||||
}
|
||||
|
||||
func (b *browser) GetGenres(ctx context.Context) (model.Genres, error) {
|
||||
genres, err := b.ds.Genre().GetAll()
|
||||
genres, err := b.ds.Genre(ctx).GetAll()
|
||||
for i, g := range genres {
|
||||
if strings.TrimSpace(g.Name) == "" {
|
||||
genres[i].Name = "<Empty>"
|
||||
@@ -195,7 +195,7 @@ func (b *browser) buildAlbumDir(al *model.Album, albumAnn *model.Annotation, tra
|
||||
}
|
||||
|
||||
func (b *browser) isArtist(ctx context.Context, id string) bool {
|
||||
found, err := b.ds.Artist().Exists(id)
|
||||
found, err := b.ds.Artist(ctx).Exists(id)
|
||||
if err != nil {
|
||||
log.Debug(ctx, "Error searching for Artist", "id", id, err)
|
||||
return false
|
||||
@@ -204,7 +204,7 @@ func (b *browser) isArtist(ctx context.Context, id string) bool {
|
||||
}
|
||||
|
||||
func (b *browser) isAlbum(ctx context.Context, id string) bool {
|
||||
found, err := b.ds.Album().Exists(id)
|
||||
found, err := b.ds.Album(ctx).Exists(id)
|
||||
if err != nil {
|
||||
log.Debug(ctx, "Error searching for Album", "id", id, err)
|
||||
return false
|
||||
@@ -212,27 +212,27 @@ func (b *browser) isAlbum(ctx context.Context, id string) bool {
|
||||
return found
|
||||
}
|
||||
|
||||
func (b *browser) retrieveArtist(id string) (a *model.Artist, as model.Albums, err error) {
|
||||
a, err = b.ds.Artist().Get(id)
|
||||
func (b *browser) retrieveArtist(ctx context.Context, id string) (a *model.Artist, as model.Albums, err error) {
|
||||
a, err = b.ds.Artist(ctx).Get(id)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading Artist %s from DB: %v", id, err)
|
||||
return
|
||||
}
|
||||
|
||||
if as, err = b.ds.Album().FindByArtist(id); err != nil {
|
||||
if as, err = b.ds.Album(ctx).FindByArtist(id); err != nil {
|
||||
err = fmt.Errorf("Error reading %s's albums from DB: %v", a.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *browser) retrieveAlbum(id string) (al *model.Album, mfs model.MediaFiles, err error) {
|
||||
al, err = b.ds.Album().Get(id)
|
||||
func (b *browser) retrieveAlbum(ctx context.Context, id string) (al *model.Album, mfs model.MediaFiles, err error) {
|
||||
al, err = b.ds.Album(ctx).Get(id)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading Album %s from DB: %v", id, err)
|
||||
return
|
||||
}
|
||||
|
||||
if mfs, err = b.ds.MediaFile().FindByAlbum(id); err != nil {
|
||||
if mfs, err = b.ds.MediaFile(ctx).FindByAlbum(id); err != nil {
|
||||
err = fmt.Errorf("Error reading %s's tracks from DB: %v", al.Name, err)
|
||||
}
|
||||
return
|
||||
|
||||
@@ -31,17 +31,17 @@ func NewCover(ds model.DataStore) Cover {
|
||||
return &cover{ds}
|
||||
}
|
||||
|
||||
func (c *cover) getCoverPath(id string) (string, error) {
|
||||
func (c *cover) getCoverPath(ctx context.Context, id string) (string, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(id, "al-"):
|
||||
id = id[3:]
|
||||
al, err := c.ds.Album().Get(id)
|
||||
al, err := c.ds.Album(ctx).Get(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return al.CoverArtPath, nil
|
||||
default:
|
||||
mf, err := c.ds.MediaFile().Get(id)
|
||||
mf, err := c.ds.MediaFile(ctx).Get(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (c *cover) getCoverPath(id string) (string, error) {
|
||||
}
|
||||
|
||||
func (c *cover) Get(ctx context.Context, id string, size int, out io.Writer) error {
|
||||
path, err := c.getCoverPath(id)
|
||||
path, err := c.getCoverPath(ctx, id)
|
||||
if err != nil && err != model.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ func TestCover(t *testing.T) {
|
||||
Init(t, false)
|
||||
|
||||
ds := &persistence.MockDataStore{}
|
||||
mockMediaFileRepo := ds.MediaFile().(*persistence.MockMediaFile)
|
||||
mockAlbumRepo := ds.Album().(*persistence.MockAlbum)
|
||||
mockMediaFileRepo := ds.MediaFile(nil).(*persistence.MockMediaFile)
|
||||
mockAlbumRepo := ds.Album(nil).(*persistence.MockAlbum)
|
||||
|
||||
cover := engine.NewCover(ds)
|
||||
out := new(bytes.Buffer)
|
||||
|
||||
@@ -31,7 +31,7 @@ type listGenerator struct {
|
||||
}
|
||||
|
||||
func (g *listGenerator) query(ctx context.Context, qo model.QueryOptions) (Entries, error) {
|
||||
albums, err := g.ds.Album().GetAll(qo)
|
||||
albums, err := g.ds.Album(ctx).GetAll(qo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func (g *listGenerator) query(ctx context.Context, qo model.QueryOptions) (Entri
|
||||
for i, al := range albums {
|
||||
albumIds[i] = al.ID
|
||||
}
|
||||
annMap, err := g.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
annMap, err := g.ds.Annotation(ctx).GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func (g *listGenerator) query(ctx context.Context, qo model.QueryOptions) (Entri
|
||||
}
|
||||
|
||||
func (g *listGenerator) queryByAnnotation(ctx context.Context, qo model.QueryOptions) (Entries, error) {
|
||||
annotations, err := g.ds.Annotation().GetAll(getUserID(ctx), model.AlbumItemType, qo)
|
||||
annotations, err := g.ds.Annotation(ctx).GetAll(getUserID(ctx), model.AlbumItemType, qo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func (g *listGenerator) queryByAnnotation(ctx context.Context, qo model.QueryOpt
|
||||
albumIds[i] = ann.ItemID
|
||||
}
|
||||
|
||||
albumMap, err := g.ds.Album().GetMap(albumIds)
|
||||
albumMap, err := g.ds.Album(ctx).GetMap(albumIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func (g *listGenerator) GetByArtist(ctx context.Context, offset int, size int) (
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetRandom(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
albums, err := g.ds.Album().GetRandom(model.QueryOptions{Max: size, Offset: offset})
|
||||
albums, err := g.ds.Album(ctx).GetRandom(model.QueryOptions{Max: size, Offset: offset})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func (g *listGenerator) getAnnotationsForAlbums(ctx context.Context, albums mode
|
||||
for i, al := range albums {
|
||||
albumIds[i] = al.ID
|
||||
}
|
||||
return g.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
return g.ds.Annotation(ctx).GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre string) (Entries, error) {
|
||||
@@ -128,14 +128,14 @@ func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre stri
|
||||
if genre != "" {
|
||||
options.Filters = map[string]interface{}{"genre": genre}
|
||||
}
|
||||
mediaFiles, err := g.ds.MediaFile().GetRandom(options)
|
||||
mediaFiles, err := g.ds.MediaFile(ctx).GetRandom(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := make(Entries, len(mediaFiles))
|
||||
for i, mf := range mediaFiles {
|
||||
ann, err := g.ds.Annotation().Get(getUserID(ctx), model.MediaItemType, mf.ID)
|
||||
ann, err := g.ds.Annotation(ctx).Get(getUserID(ctx), model.MediaItemType, mf.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre stri
|
||||
|
||||
func (g *listGenerator) GetStarred(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
qo := model.QueryOptions{Offset: offset, Max: size, Sort: "starred_at", Order: "desc"}
|
||||
albums, err := g.ds.Album().GetStarred(getUserID(ctx), qo)
|
||||
albums, err := g.ds.Album(ctx).GetStarred(getUserID(ctx), qo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -161,17 +161,17 @@ func (g *listGenerator) GetStarred(ctx context.Context, offset int, size int) (E
|
||||
func (g *listGenerator) GetAllStarred(ctx context.Context) (artists Entries, albums Entries, mediaFiles Entries, err error) {
|
||||
options := model.QueryOptions{Sort: "starred_at", Order: "desc"}
|
||||
|
||||
ars, err := g.ds.Artist().GetStarred(getUserID(ctx), options)
|
||||
ars, err := g.ds.Artist(ctx).GetStarred(getUserID(ctx), options)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
als, err := g.ds.Album().GetStarred(getUserID(ctx), options)
|
||||
als, err := g.ds.Album(ctx).GetStarred(getUserID(ctx), options)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
mfs, err := g.ds.MediaFile().GetStarred(getUserID(ctx), options)
|
||||
mfs, err := g.ds.MediaFile(ctx).GetStarred(getUserID(ctx), options)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func (g *listGenerator) GetAllStarred(ctx context.Context) (artists Entries, alb
|
||||
for _, mf := range mfs {
|
||||
mfIds = append(mfIds, mf.ID)
|
||||
}
|
||||
trackAnnMap, err := g.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, mfIds)
|
||||
trackAnnMap, err := g.ds.Annotation(ctx).GetMap(getUserID(ctx), model.MediaItemType, mfIds)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func (g *listGenerator) GetAllStarred(ctx context.Context) (artists Entries, alb
|
||||
for _, ar := range ars {
|
||||
artistIds = append(artistIds, ar.ID)
|
||||
}
|
||||
artistAnnMap, err := g.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, artistIds)
|
||||
artistAnnMap, err := g.ds.Annotation(ctx).GetMap(getUserID(ctx), model.MediaItemType, artistIds)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -213,11 +213,11 @@ func (g *listGenerator) GetNowPlaying(ctx context.Context) (Entries, error) {
|
||||
}
|
||||
entries := make(Entries, len(npInfo))
|
||||
for i, np := range npInfo {
|
||||
mf, err := g.ds.MediaFile().Get(np.TrackID)
|
||||
mf, err := g.ds.MediaFile(ctx).Get(np.TrackID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ann, err := g.ds.Annotation().Get(getUserID(ctx), model.MediaItemType, mf.ID)
|
||||
ann, err := g.ds.Annotation(ctx).Get(getUserID(ctx), model.MediaItemType, mf.ID)
|
||||
entries[i] = FromMediaFile(mf, ann)
|
||||
entries[i].UserName = np.Username
|
||||
entries[i].MinutesAgo = int(time.Now().Sub(np.Start).Minutes())
|
||||
|
||||
@@ -3,7 +3,6 @@ package engine
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/deluan/navidrome/consts"
|
||||
"github.com/deluan/navidrome/model"
|
||||
"github.com/deluan/navidrome/utils"
|
||||
)
|
||||
@@ -30,7 +29,7 @@ func (p *playlists) Create(ctx context.Context, playlistId, name string, ids []s
|
||||
var err error
|
||||
// If playlistID is present, override tracks
|
||||
if playlistId != "" {
|
||||
pls, err = p.ds.Playlist().Get(playlistId)
|
||||
pls, err = p.ds.Playlist(ctx).Get(playlistId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -48,20 +47,19 @@ func (p *playlists) Create(ctx context.Context, playlistId, name string, ids []s
|
||||
pls.Tracks = append(pls.Tracks, model.MediaFile{ID: id})
|
||||
}
|
||||
|
||||
return p.ds.Playlist().Put(pls)
|
||||
return p.ds.Playlist(ctx).Put(pls)
|
||||
}
|
||||
|
||||
func (p *playlists) getUser(ctx context.Context) string {
|
||||
owner := consts.InitialUserName
|
||||
user, ok := ctx.Value("user").(*model.User)
|
||||
if ok {
|
||||
owner = user.UserName
|
||||
return user.UserName
|
||||
}
|
||||
return owner
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *playlists) Delete(ctx context.Context, playlistId string) error {
|
||||
pls, err := p.ds.Playlist().Get(playlistId)
|
||||
pls, err := p.ds.Playlist(ctx).Get(playlistId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -70,11 +68,11 @@ func (p *playlists) Delete(ctx context.Context, playlistId string) error {
|
||||
if owner != pls.Owner {
|
||||
return model.ErrNotAuthorized
|
||||
}
|
||||
return p.ds.Playlist().Delete(playlistId)
|
||||
return p.ds.Playlist(nil).Delete(playlistId)
|
||||
}
|
||||
|
||||
func (p *playlists) Update(ctx context.Context, playlistId string, name *string, idsToAdd []string, idxToRemove []int) error {
|
||||
pls, err := p.ds.Playlist().Get(playlistId)
|
||||
pls, err := p.ds.Playlist(ctx).Get(playlistId)
|
||||
|
||||
owner := p.getUser(ctx)
|
||||
if owner != pls.Owner {
|
||||
@@ -100,11 +98,11 @@ func (p *playlists) Update(ctx context.Context, playlistId string, name *string,
|
||||
}
|
||||
pls.Tracks = newTracks
|
||||
|
||||
return p.ds.Playlist().Put(pls)
|
||||
return p.ds.Playlist(ctx).Put(pls)
|
||||
}
|
||||
|
||||
func (p *playlists) GetAll(ctx context.Context) (model.Playlists, error) {
|
||||
return p.ds.Playlist().GetAll(model.QueryOptions{})
|
||||
return p.ds.Playlist(ctx).GetAll(model.QueryOptions{})
|
||||
}
|
||||
|
||||
type PlaylistInfo struct {
|
||||
@@ -119,7 +117,7 @@ type PlaylistInfo struct {
|
||||
}
|
||||
|
||||
func (p *playlists) Get(ctx context.Context, id string) (*PlaylistInfo, error) {
|
||||
pl, err := p.ds.Playlist().GetWithTracks(id)
|
||||
pl, err := p.ds.Playlist(ctx).GetWithTracks(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -141,7 +139,7 @@ func (p *playlists) Get(ctx context.Context, id string) (*PlaylistInfo, error) {
|
||||
mfIds = append(mfIds, mf.ID)
|
||||
}
|
||||
|
||||
annMap, err := p.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, mfIds)
|
||||
annMap, err := p.ds.Annotation(ctx).GetMap(getUserID(ctx), model.MediaItemType, mfIds)
|
||||
|
||||
for i, mf := range pl.Tracks {
|
||||
ann := annMap[mf.ID]
|
||||
|
||||
@@ -21,14 +21,14 @@ type ratings struct {
|
||||
}
|
||||
|
||||
func (r ratings) SetRating(ctx context.Context, id string, rating int) error {
|
||||
exist, err := r.ds.Album().Exists(id)
|
||||
exist, err := r.ds.Album(ctx).Exists(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
return r.ds.Annotation().SetRating(rating, getUserID(ctx), model.AlbumItemType, id)
|
||||
return r.ds.Annotation(ctx).SetRating(rating, getUserID(ctx), model.AlbumItemType, id)
|
||||
}
|
||||
return r.ds.Annotation().SetRating(rating, getUserID(ctx), model.MediaItemType, id)
|
||||
return r.ds.Annotation(ctx).SetRating(rating, getUserID(ctx), model.MediaItemType, id)
|
||||
}
|
||||
|
||||
func (r ratings) SetStar(ctx context.Context, star bool, ids ...string) error {
|
||||
@@ -40,29 +40,29 @@ func (r ratings) SetStar(ctx context.Context, star bool, ids ...string) error {
|
||||
|
||||
return r.ds.WithTx(func(tx model.DataStore) error {
|
||||
for _, id := range ids {
|
||||
exist, err := r.ds.Album().Exists(id)
|
||||
exist, err := r.ds.Album(ctx).Exists(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
err = tx.Annotation().SetStar(star, userId, model.AlbumItemType, ids...)
|
||||
err = tx.Annotation(ctx).SetStar(star, userId, model.AlbumItemType, ids...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
exist, err = r.ds.Artist().Exists(id)
|
||||
exist, err = r.ds.Artist(ctx).Exists(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
err = tx.Annotation().SetStar(star, userId, model.ArtistItemType, ids...)
|
||||
err = tx.Annotation(ctx).SetStar(star, userId, model.ArtistItemType, ids...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = tx.Annotation().SetStar(star, userId, model.MediaItemType, ids...)
|
||||
err = tx.Annotation(ctx).SetStar(star, userId, model.MediaItemType, ids...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -29,15 +29,15 @@ func (s *scrobbler) Register(ctx context.Context, playerId int, trackId string,
|
||||
var mf *model.MediaFile
|
||||
var err error
|
||||
err = s.ds.WithTx(func(tx model.DataStore) error {
|
||||
mf, err = s.ds.MediaFile().Get(trackId)
|
||||
mf, err = s.ds.MediaFile(ctx).Get(trackId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.ds.Annotation().IncPlayCount(userId, model.MediaItemType, trackId, playTime)
|
||||
err = s.ds.Annotation(ctx).IncPlayCount(userId, model.MediaItemType, trackId, playTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.ds.Annotation().IncPlayCount(userId, model.AlbumItemType, mf.AlbumID, playTime)
|
||||
err = s.ds.Annotation(ctx).IncPlayCount(userId, model.AlbumItemType, mf.AlbumID, playTime)
|
||||
return err
|
||||
})
|
||||
return mf, err
|
||||
@@ -45,7 +45,7 @@ func (s *scrobbler) Register(ctx context.Context, playerId int, trackId string,
|
||||
|
||||
// TODO Validate if NowPlaying still works after all refactorings
|
||||
func (s *scrobbler) NowPlaying(ctx context.Context, playerId int, playerName, trackId, username string) (*model.MediaFile, error) {
|
||||
mf, err := s.ds.MediaFile().Get(trackId)
|
||||
mf, err := s.ds.MediaFile(ctx).Get(trackId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func NewSearch(ds model.DataStore) Search {
|
||||
|
||||
func (s *search) SearchArtist(ctx context.Context, q string, offset int, size int) (Entries, error) {
|
||||
q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*")))
|
||||
artists, err := s.ds.Artist().Search(q, offset, size)
|
||||
artists, err := s.ds.Artist(ctx).Search(q, offset, size)
|
||||
if len(artists) == 0 || err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func (s *search) SearchArtist(ctx context.Context, q string, offset int, size in
|
||||
for i, al := range artists {
|
||||
artistIds[i] = al.ID
|
||||
}
|
||||
annMap, err := s.ds.Annotation().GetMap(getUserID(ctx), model.ArtistItemType, artistIds)
|
||||
annMap, err := s.ds.Annotation(ctx).GetMap(getUserID(ctx), model.ArtistItemType, artistIds)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func (s *search) SearchArtist(ctx context.Context, q string, offset int, size in
|
||||
|
||||
func (s *search) SearchAlbum(ctx context.Context, q string, offset int, size int) (Entries, error) {
|
||||
q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*")))
|
||||
albums, err := s.ds.Album().Search(q, offset, size)
|
||||
albums, err := s.ds.Album(ctx).Search(q, offset, size)
|
||||
if len(albums) == 0 || err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (s *search) SearchAlbum(ctx context.Context, q string, offset int, size int
|
||||
for i, al := range albums {
|
||||
albumIds[i] = al.ID
|
||||
}
|
||||
annMap, err := s.ds.Annotation().GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
annMap, err := s.ds.Annotation(ctx).GetMap(getUserID(ctx), model.AlbumItemType, albumIds)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func (s *search) SearchAlbum(ctx context.Context, q string, offset int, size int
|
||||
|
||||
func (s *search) SearchSong(ctx context.Context, q string, offset int, size int) (Entries, error) {
|
||||
q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*")))
|
||||
mediaFiles, err := s.ds.MediaFile().Search(q, offset, size)
|
||||
mediaFiles, err := s.ds.MediaFile(ctx).Search(q, offset, size)
|
||||
if len(mediaFiles) == 0 || err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (s *search) SearchSong(ctx context.Context, q string, offset int, size int)
|
||||
for i, mf := range mediaFiles {
|
||||
trackIds[i] = mf.ID
|
||||
}
|
||||
annMap, err := s.ds.Annotation().GetMap(getUserID(ctx), model.MediaItemType, trackIds)
|
||||
annMap, err := s.ds.Annotation(ctx).GetMap(getUserID(ctx), model.MediaItemType, trackIds)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
)
|
||||
|
||||
@@ -24,7 +23,7 @@ type users struct {
|
||||
}
|
||||
|
||||
func (u *users) Authenticate(ctx context.Context, username, pass, token, salt string) (*model.User, error) {
|
||||
user, err := u.ds.User().FindByUsername(username)
|
||||
user, err := u.ds.User(ctx).FindByUsername(username)
|
||||
if err == model.ErrNotFound {
|
||||
return nil, model.ErrInvalidAuth
|
||||
}
|
||||
@@ -49,11 +48,12 @@ func (u *users) Authenticate(ctx context.Context, username, pass, token, salt st
|
||||
if !valid {
|
||||
return nil, model.ErrInvalidAuth
|
||||
}
|
||||
go func() {
|
||||
err := u.ds.User().UpdateLastAccessAt(user.ID)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Could not update user's lastAccessAt", "user", user.UserName)
|
||||
}
|
||||
}()
|
||||
// TODO: Find a way to update LastAccessAt without causing too much retention in the DB
|
||||
//go func() {
|
||||
// err := u.ds.User(ctx).UpdateLastAccessAt(user.ID)
|
||||
// if err != nil {
|
||||
// log.Error(ctx, "Could not update user's lastAccessAt", "user", user.UserName)
|
||||
// }
|
||||
//}()
|
||||
return user, nil
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -21,7 +21,7 @@ require (
|
||||
github.com/koding/multiconfig v0.0.0-20170327155832-26b6dfd3a84a
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/lib/pq v1.3.0
|
||||
github.com/mattn/go-sqlite3 v2.0.2+incompatible
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
|
||||
github.com/onsi/ginkgo v1.11.0
|
||||
github.com/onsi/gomega v1.8.1
|
||||
|
||||
4
go.sum
4
go.sum
@@ -85,8 +85,8 @@ github.com/lib/pq v1.0.0/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/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U=
|
||||
github.com/mattn/go-sqlite3 v2.0.2+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/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY=
|
||||
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
)
|
||||
|
||||
@@ -22,17 +24,17 @@ type ResourceRepository interface {
|
||||
}
|
||||
|
||||
type DataStore interface {
|
||||
Album() AlbumRepository
|
||||
Artist() ArtistRepository
|
||||
MediaFile() MediaFileRepository
|
||||
MediaFolder() MediaFolderRepository
|
||||
Genre() GenreRepository
|
||||
Playlist() PlaylistRepository
|
||||
Property() PropertyRepository
|
||||
User() UserRepository
|
||||
Annotation() AnnotationRepository
|
||||
Album(ctx context.Context) AlbumRepository
|
||||
Artist(ctx context.Context) ArtistRepository
|
||||
MediaFile(ctx context.Context) MediaFileRepository
|
||||
MediaFolder(ctx context.Context) MediaFolderRepository
|
||||
Genre(ctx context.Context) GenreRepository
|
||||
Playlist(ctx context.Context) PlaylistRepository
|
||||
Property(ctx context.Context) PropertyRepository
|
||||
User(ctx context.Context) UserRepository
|
||||
Annotation(ctx context.Context) AnnotationRepository
|
||||
|
||||
Resource(model interface{}) ResourceRepository
|
||||
Resource(ctx context.Context, model interface{}) ResourceRepository
|
||||
|
||||
WithTx(func(tx DataStore) error) error
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package persistence
|
||||
|
||||
import "github.com/deluan/navidrome/model"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/deluan/navidrome/model"
|
||||
)
|
||||
|
||||
type MockDataStore struct {
|
||||
MockedGenre model.GenreRepository
|
||||
@@ -10,54 +14,54 @@ type MockDataStore struct {
|
||||
MockedUser model.UserRepository
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Album() model.AlbumRepository {
|
||||
func (db *MockDataStore) Album(context.Context) model.AlbumRepository {
|
||||
if db.MockedAlbum == nil {
|
||||
db.MockedAlbum = CreateMockAlbumRepo()
|
||||
}
|
||||
return db.MockedAlbum
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Artist() model.ArtistRepository {
|
||||
func (db *MockDataStore) Artist(context.Context) model.ArtistRepository {
|
||||
if db.MockedArtist == nil {
|
||||
db.MockedArtist = CreateMockArtistRepo()
|
||||
}
|
||||
return db.MockedArtist
|
||||
}
|
||||
|
||||
func (db *MockDataStore) MediaFile() model.MediaFileRepository {
|
||||
func (db *MockDataStore) MediaFile(context.Context) model.MediaFileRepository {
|
||||
if db.MockedMediaFile == nil {
|
||||
db.MockedMediaFile = CreateMockMediaFileRepo()
|
||||
}
|
||||
return db.MockedMediaFile
|
||||
}
|
||||
|
||||
func (db *MockDataStore) MediaFolder() model.MediaFolderRepository {
|
||||
func (db *MockDataStore) MediaFolder(context.Context) model.MediaFolderRepository {
|
||||
return struct{ model.MediaFolderRepository }{}
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Genre() model.GenreRepository {
|
||||
func (db *MockDataStore) Genre(context.Context) model.GenreRepository {
|
||||
if db.MockedGenre != nil {
|
||||
return db.MockedGenre
|
||||
}
|
||||
return struct{ model.GenreRepository }{}
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Playlist() model.PlaylistRepository {
|
||||
func (db *MockDataStore) Playlist(context.Context) model.PlaylistRepository {
|
||||
return struct{ model.PlaylistRepository }{}
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Property() model.PropertyRepository {
|
||||
func (db *MockDataStore) Property(context.Context) model.PropertyRepository {
|
||||
return struct{ model.PropertyRepository }{}
|
||||
}
|
||||
|
||||
func (db *MockDataStore) User() model.UserRepository {
|
||||
func (db *MockDataStore) User(context.Context) model.UserRepository {
|
||||
if db.MockedUser == nil {
|
||||
db.MockedUser = &mockedUserRepo{}
|
||||
}
|
||||
return db.MockedUser
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Annotation() model.AnnotationRepository {
|
||||
func (db *MockDataStore) Annotation(context.Context) model.AnnotationRepository {
|
||||
return struct{ model.AnnotationRepository }{}
|
||||
}
|
||||
|
||||
@@ -65,7 +69,7 @@ func (db *MockDataStore) WithTx(block func(db model.DataStore) error) error {
|
||||
return block(db)
|
||||
}
|
||||
|
||||
func (db *MockDataStore) Resource(m interface{}) model.ResourceRepository {
|
||||
func (db *MockDataStore) Resource(ctx context.Context, m interface{}) model.ResourceRepository {
|
||||
return struct{ model.ResourceRepository }{}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -41,43 +42,43 @@ func New() model.DataStore {
|
||||
return &SQLStore{}
|
||||
}
|
||||
|
||||
func (db *SQLStore) Album() model.AlbumRepository {
|
||||
func (db *SQLStore) Album(context.Context) model.AlbumRepository {
|
||||
return NewAlbumRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Artist() model.ArtistRepository {
|
||||
func (db *SQLStore) Artist(context.Context) model.ArtistRepository {
|
||||
return NewArtistRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) MediaFile() model.MediaFileRepository {
|
||||
func (db *SQLStore) MediaFile(context.Context) model.MediaFileRepository {
|
||||
return NewMediaFileRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) MediaFolder() model.MediaFolderRepository {
|
||||
func (db *SQLStore) MediaFolder(context.Context) model.MediaFolderRepository {
|
||||
return NewMediaFolderRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Genre() model.GenreRepository {
|
||||
func (db *SQLStore) Genre(context.Context) model.GenreRepository {
|
||||
return NewGenreRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Playlist() model.PlaylistRepository {
|
||||
func (db *SQLStore) Playlist(context.Context) model.PlaylistRepository {
|
||||
return NewPlaylistRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Property() model.PropertyRepository {
|
||||
func (db *SQLStore) Property(context.Context) model.PropertyRepository {
|
||||
return NewPropertyRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) User() model.UserRepository {
|
||||
func (db *SQLStore) User(context.Context) model.UserRepository {
|
||||
return NewUserRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Annotation() model.AnnotationRepository {
|
||||
func (db *SQLStore) Annotation(context.Context) model.AnnotationRepository {
|
||||
return NewAnnotationRepository(db.getOrmer())
|
||||
}
|
||||
|
||||
func (db *SQLStore) Resource(model interface{}) model.ResourceRepository {
|
||||
func (db *SQLStore) Resource(ctx context.Context, model interface{}) model.ResourceRepository {
|
||||
return NewResource(db.getOrmer(), model, getMappedModel(model))
|
||||
}
|
||||
|
||||
|
||||
@@ -63,21 +63,21 @@ var _ = Describe("Initialize test DB", func() {
|
||||
BeforeSuite(func() {
|
||||
conf.Server.DbPath = ":memory:"
|
||||
ds := New()
|
||||
artistRepo := ds.Artist()
|
||||
artistRepo := ds.Artist(nil)
|
||||
for _, a := range testArtists {
|
||||
err := artistRepo.Put(&a)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
albumRepository := ds.Album()
|
||||
albumRepository := ds.Album(nil)
|
||||
for _, a := range testAlbums {
|
||||
err := albumRepository.Put(&a)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
mediaFileRepository := ds.MediaFile()
|
||||
mediaFileRepository := ds.MediaFile(nil)
|
||||
for _, s := range testSongs {
|
||||
err := mediaFileRepository.Put(&s)
|
||||
if err != nil {
|
||||
|
||||
@@ -83,6 +83,7 @@ func probe(inputs []string) (map[string]*Metadata, error) {
|
||||
infos := parseOutput(string(output))
|
||||
for file, info := range infos {
|
||||
md, err := extractMetadata(file, info)
|
||||
// Skip files with errors
|
||||
if err == nil {
|
||||
mds[file] = md
|
||||
}
|
||||
@@ -117,9 +118,16 @@ func parseOutput(output string) map[string]string {
|
||||
func extractMetadata(filePath, info string) (*Metadata, error) {
|
||||
m := &Metadata{filePath: filePath, tags: map[string]string{}}
|
||||
m.suffix = strings.ToLower(strings.TrimPrefix(path.Ext(filePath), "."))
|
||||
var err error
|
||||
m.fileInfo, err = os.Stat(filePath)
|
||||
if err != nil {
|
||||
log.Warn("Error stating file. Skipping", "filePath", filePath, err)
|
||||
return nil, errors.New("error stating file")
|
||||
}
|
||||
|
||||
m.parseInfo(info)
|
||||
m.fileInfo, _ = os.Stat(filePath)
|
||||
if len(m.tags) == 0 {
|
||||
log.Trace("Not a media file. Skipping", "filePath", filePath)
|
||||
return nil, errors.New("not a media file")
|
||||
}
|
||||
return m, nil
|
||||
|
||||
@@ -60,7 +60,7 @@ func (s *Scanner) RescanAll(fullRescan bool) error {
|
||||
func (s *Scanner) Status() []StatusInfo { return nil }
|
||||
|
||||
func (s *Scanner) getLastModifiedSince(folder string) time.Time {
|
||||
ms, err := s.ds.Property().Get(model.PropLastScan + "-" + folder)
|
||||
ms, err := s.ds.Property(nil).Get(model.PropLastScan + "-" + folder)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
@@ -73,11 +73,11 @@ func (s *Scanner) getLastModifiedSince(folder string) time.Time {
|
||||
|
||||
func (s *Scanner) updateLastModifiedSince(folder string, t time.Time) {
|
||||
millis := t.UnixNano() / int64(time.Millisecond)
|
||||
s.ds.Property().Put(model.PropLastScan+"-"+folder, fmt.Sprint(millis))
|
||||
s.ds.Property(nil).Put(model.PropLastScan+"-"+folder, fmt.Sprint(millis))
|
||||
}
|
||||
|
||||
func (s *Scanner) loadFolders() {
|
||||
fs, _ := s.ds.MediaFolder().GetAll()
|
||||
fs, _ := s.ds.MediaFolder(nil).GetAll()
|
||||
for _, f := range fs {
|
||||
log.Info("Configuring Media Folder", "name", f.Name, "path", f.Path)
|
||||
s.folders[f.Path] = NewTagScanner(f.Path, s.ds)
|
||||
|
||||
@@ -58,16 +58,16 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time) erro
|
||||
updatedAlbums := map[string]bool{}
|
||||
|
||||
for _, c := range changed {
|
||||
err := s.processChangedDir(c, updatedArtists, updatedAlbums)
|
||||
err := s.processChangedDir(ctx, c, updatedArtists, updatedAlbums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(updatedAlbums)+len(updatedArtists) > 100 {
|
||||
err = s.refreshAlbums(updatedAlbums)
|
||||
err = s.refreshAlbums(ctx, updatedAlbums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.refreshArtists(updatedArtists)
|
||||
err = s.refreshArtists(ctx, updatedArtists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -76,16 +76,16 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time) erro
|
||||
}
|
||||
}
|
||||
for _, c := range deleted {
|
||||
err := s.processDeletedDir(c, updatedArtists, updatedAlbums)
|
||||
err := s.processDeletedDir(ctx, c, updatedArtists, updatedAlbums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(updatedAlbums)+len(updatedArtists) > 100 {
|
||||
err = s.refreshAlbums(updatedAlbums)
|
||||
err = s.refreshAlbums(ctx, updatedAlbums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.refreshArtists(updatedArtists)
|
||||
err = s.refreshArtists(ctx, updatedArtists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -94,22 +94,22 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time) erro
|
||||
}
|
||||
}
|
||||
|
||||
err = s.refreshAlbums(updatedAlbums)
|
||||
err = s.refreshAlbums(ctx, updatedAlbums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.refreshArtists(updatedArtists)
|
||||
err = s.refreshArtists(ctx, updatedArtists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ds.Album().PurgeEmpty()
|
||||
err = s.ds.Album(ctx).PurgeEmpty()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ds.Artist().PurgeEmpty()
|
||||
err = s.ds.Artist(ctx).PurgeEmpty()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -117,30 +117,30 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TagScanner) refreshAlbums(updatedAlbums map[string]bool) error {
|
||||
func (s *TagScanner) refreshAlbums(ctx context.Context, updatedAlbums map[string]bool) error {
|
||||
var ids []string
|
||||
for id := range updatedAlbums {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return s.ds.Album().Refresh(ids...)
|
||||
return s.ds.Album(ctx).Refresh(ids...)
|
||||
}
|
||||
|
||||
func (s *TagScanner) refreshArtists(updatedArtists map[string]bool) error {
|
||||
func (s *TagScanner) refreshArtists(ctx context.Context, updatedArtists map[string]bool) error {
|
||||
var ids []string
|
||||
for id := range updatedArtists {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return s.ds.Artist().Refresh(ids...)
|
||||
return s.ds.Artist(ctx).Refresh(ids...)
|
||||
}
|
||||
|
||||
func (s *TagScanner) processChangedDir(dir string, updatedArtists map[string]bool, updatedAlbums map[string]bool) error {
|
||||
func (s *TagScanner) processChangedDir(ctx context.Context, dir string, updatedArtists map[string]bool, updatedAlbums map[string]bool) error {
|
||||
dir = path.Join(s.rootFolder, dir)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Load folder's current tracks from DB into a map
|
||||
currentTracks := map[string]model.MediaFile{}
|
||||
ct, err := s.ds.MediaFile().FindByPath(dir)
|
||||
ct, err := s.ds.MediaFile(ctx).FindByPath(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -168,7 +168,7 @@ func (s *TagScanner) processChangedDir(dir string, updatedArtists map[string]boo
|
||||
for _, n := range newTracks {
|
||||
c, ok := currentTracks[n.ID]
|
||||
if !ok || (ok && n.UpdatedAt.After(c.UpdatedAt)) {
|
||||
err := s.ds.MediaFile().Put(&n)
|
||||
err := s.ds.MediaFile(ctx).Put(&n)
|
||||
updatedArtists[n.ArtistID] = true
|
||||
updatedAlbums[n.AlbumID] = true
|
||||
numUpdatedTracks++
|
||||
@@ -182,7 +182,7 @@ func (s *TagScanner) processChangedDir(dir string, updatedArtists map[string]boo
|
||||
// Remaining tracks from DB that are not in the folder are deleted
|
||||
for id := range currentTracks {
|
||||
numPurgedTracks++
|
||||
if err := s.ds.MediaFile().Delete(id); err != nil {
|
||||
if err := s.ds.MediaFile(ctx).Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -191,10 +191,10 @@ func (s *TagScanner) processChangedDir(dir string, updatedArtists map[string]boo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TagScanner) processDeletedDir(dir string, updatedArtists map[string]bool, updatedAlbums map[string]bool) error {
|
||||
func (s *TagScanner) processDeletedDir(ctx context.Context, dir string, updatedArtists map[string]bool, updatedAlbums map[string]bool) error {
|
||||
dir = path.Join(s.rootFolder, dir)
|
||||
|
||||
ct, err := s.ds.MediaFile().FindByPath(dir)
|
||||
ct, err := s.ds.MediaFile(ctx).FindByPath(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -203,7 +203,7 @@ func (s *TagScanner) processDeletedDir(dir string, updatedArtists map[string]boo
|
||||
updatedAlbums[t.AlbumID] = true
|
||||
}
|
||||
|
||||
return s.ds.MediaFile().DeleteByPath(dir)
|
||||
return s.ds.MediaFile(ctx).DeleteByPath(dir)
|
||||
}
|
||||
|
||||
func (s *TagScanner) loadTracks(dirPath string) (model.MediaFiles, error) {
|
||||
|
||||
@@ -64,7 +64,7 @@ func (app *Router) routes() http.Handler {
|
||||
|
||||
func (app *Router) R(r chi.Router, pathPrefix string, model interface{}) {
|
||||
constructor := func(ctx context.Context) rest.Repository {
|
||||
return app.ds.Resource(model)
|
||||
return app.ds.Resource(ctx, model)
|
||||
}
|
||||
r.Route(pathPrefix, func(r chi.Router) {
|
||||
r.Get("/", rest.GetAll(constructor))
|
||||
|
||||
@@ -41,7 +41,7 @@ func Login(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func handleLogin(ds model.DataStore, username string, password string, w http.ResponseWriter, r *http.Request) {
|
||||
user, err := validateLogin(ds.User(), username, password)
|
||||
user, err := validateLogin(ds.User(r.Context()), username, password)
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authentication user. Please try again")
|
||||
return
|
||||
@@ -89,7 +89,7 @@ func CreateAdmin(ds model.DataStore) func(w http.ResponseWriter, r *http.Request
|
||||
rest.RespondWithError(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
c, err := ds.User().CountAll()
|
||||
c, err := ds.User(r.Context()).CountAll()
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -98,7 +98,7 @@ func CreateAdmin(ds model.DataStore) func(w http.ResponseWriter, r *http.Request
|
||||
rest.RespondWithError(w, http.StatusForbidden, "Cannot create another first admin")
|
||||
return
|
||||
}
|
||||
err = createDefaultUser(ds, username, password)
|
||||
err = createDefaultUser(r.Context(), ds, username, password)
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -107,9 +107,9 @@ func CreateAdmin(ds model.DataStore) func(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
func createDefaultUser(ds model.DataStore, username, password string) error {
|
||||
func createDefaultUser(ctx context.Context, ds model.DataStore, username, password string) error {
|
||||
id, _ := uuid.NewRandom()
|
||||
log.Warn("Creating initial user", "user", consts.InitialUserName)
|
||||
log.Warn("Creating initial user", "user", username)
|
||||
initialUser := model.User{
|
||||
ID: id.String(),
|
||||
UserName: username,
|
||||
@@ -118,7 +118,7 @@ func createDefaultUser(ds model.DataStore, username, password string) error {
|
||||
Password: password,
|
||||
IsAdmin: true,
|
||||
}
|
||||
err := ds.User().Put(&initialUser)
|
||||
err := ds.User(ctx).Put(&initialUser)
|
||||
if err != nil {
|
||||
log.Error("Could not create initial user", "user", initialUser, err)
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func createDefaultUser(ds model.DataStore, username, password string) error {
|
||||
|
||||
func initTokenAuth(ds model.DataStore) {
|
||||
once.Do(func() {
|
||||
secret, err := ds.Property().DefaultGet(consts.JWTSecretKey, "not so secret")
|
||||
secret, err := ds.Property(nil).DefaultGet(consts.JWTSecretKey, "not so secret")
|
||||
if err != nil {
|
||||
log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err)
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func getToken(ds model.DataStore, ctx context.Context) (*jwt.Token, error) {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
c, err := ds.User().CountAll()
|
||||
c, err := ds.User(ctx).CountAll()
|
||||
firstTime := c == 0 && err == nil
|
||||
if firstTime {
|
||||
return nil, ErrFirstTime
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func initialSetup(ds model.DataStore) {
|
||||
_ = ds.WithTx(func(tx model.DataStore) error {
|
||||
_, err := ds.Property().Get(consts.InitialSetupFlagKey)
|
||||
_, err := ds.Property(nil).Get(consts.InitialSetupFlagKey)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -20,19 +20,19 @@ func initialSetup(ds model.DataStore) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ds.Property().Put(consts.InitialSetupFlagKey, time.Now().String())
|
||||
err = ds.Property(nil).Put(consts.InitialSetupFlagKey, time.Now().String())
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func createJWTSecret(ds model.DataStore) error {
|
||||
_, err := ds.Property().Get(consts.JWTSecretKey)
|
||||
_, err := ds.Property(nil).Get(consts.JWTSecretKey)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
jwtSecret, _ := uuid.NewRandom()
|
||||
log.Warn("Creating JWT secret, used for encrypting UI sessions")
|
||||
err = ds.Property().Put(consts.JWTSecretKey, jwtSecret.String())
|
||||
err = ds.Property(nil).Put(consts.JWTSecretKey, jwtSecret.String())
|
||||
if err != nil {
|
||||
log.Error("Could not save JWT secret in DB", err)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
|
||||
Describe("GetAlbumList", func() {
|
||||
It("should return list of the type specified", func() {
|
||||
r := newTestRequest("type=newest", "offset=10", "size=20")
|
||||
r := newGetRequest("type=newest", "offset=10", "size=20")
|
||||
listGen.data = engine.Entries{
|
||||
{Id: "1"}, {Id: "2"},
|
||||
}
|
||||
@@ -54,7 +54,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
})
|
||||
|
||||
It("should fail if missing type parameter", func() {
|
||||
r := newTestRequest()
|
||||
r := newGetRequest()
|
||||
_, err := controller.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).To(MatchError("Required string parameter 'type' is not present"))
|
||||
@@ -62,7 +62,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
|
||||
It("should return error if call fails", func() {
|
||||
listGen.err = errors.New("some issue")
|
||||
r := newTestRequest("type=newest")
|
||||
r := newGetRequest("type=newest")
|
||||
|
||||
_, err := controller.GetAlbumList(w, r)
|
||||
|
||||
@@ -72,7 +72,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
|
||||
Describe("GetAlbumList2", func() {
|
||||
It("should return list of the type specified", func() {
|
||||
r := newTestRequest("type=newest", "offset=10", "size=20")
|
||||
r := newGetRequest("type=newest", "offset=10", "size=20")
|
||||
listGen.data = engine.Entries{
|
||||
{Id: "1"}, {Id: "2"},
|
||||
}
|
||||
@@ -86,7 +86,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
})
|
||||
|
||||
It("should fail if missing type parameter", func() {
|
||||
r := newTestRequest()
|
||||
r := newGetRequest()
|
||||
_, err := controller.GetAlbumList2(w, r)
|
||||
|
||||
Expect(err).To(MatchError("Required string parameter 'type' is not present"))
|
||||
@@ -94,7 +94,7 @@ var _ = Describe("AlbumListController", func() {
|
||||
|
||||
It("should return error if call fails", func() {
|
||||
listGen.err = errors.New("some issue")
|
||||
r := newTestRequest("type=newest")
|
||||
r := newGetRequest("type=newest")
|
||||
|
||||
_, err := controller.GetAlbumList2(w, r)
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ func (api *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (api *Router) routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(postFormToQueryParams)
|
||||
r.Use(checkRequiredParameters)
|
||||
|
||||
// Add validation middleware if not disabled
|
||||
@@ -150,11 +151,12 @@ func HGone(r chi.Router, path string) {
|
||||
}
|
||||
|
||||
func SendError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
response := &responses.Subsonic{Version: Version, Status: "fail"}
|
||||
response := NewResponse()
|
||||
code := responses.ErrorGeneric
|
||||
if e, ok := err.(SubsonicError); ok {
|
||||
code = e.code
|
||||
}
|
||||
response.Status = "fail"
|
||||
response.Error = &responses.Error{Code: code, Message: err.Error()}
|
||||
|
||||
SendResponse(w, r, response)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/navidrome/consts"
|
||||
"github.com/deluan/navidrome/engine"
|
||||
"github.com/deluan/navidrome/model"
|
||||
"github.com/deluan/navidrome/server/subsonic/responses"
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func NewResponse() *responses.Subsonic {
|
||||
return &responses.Subsonic{Status: "ok", Version: Version}
|
||||
return &responses.Subsonic{Status: "ok", Version: Version, Type: consts.AppName, ServerVersion: consts.Version()}
|
||||
}
|
||||
|
||||
func RequiredParamString(r *http.Request, param string, msg string) (string, error) {
|
||||
|
||||
@@ -42,7 +42,7 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
Describe("GetCoverArt", func() {
|
||||
It("should return data for that id", func() {
|
||||
cover.data = "image data"
|
||||
r := newTestRequest("id=34", "size=128")
|
||||
r := newGetRequest("id=34", "size=128")
|
||||
_, err := controller.GetCoverArt(w, r)
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
@@ -52,7 +52,7 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
})
|
||||
|
||||
It("should fail if missing id parameter", func() {
|
||||
r := newTestRequest()
|
||||
r := newGetRequest()
|
||||
_, err := controller.GetCoverArt(w, r)
|
||||
|
||||
Expect(err).To(MatchError("id parameter required"))
|
||||
@@ -60,7 +60,7 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
|
||||
It("should fail when the file is not found", func() {
|
||||
cover.err = model.ErrNotFound
|
||||
r := newTestRequest("id=34", "size=128")
|
||||
r := newGetRequest("id=34", "size=128")
|
||||
_, err := controller.GetCoverArt(w, r)
|
||||
|
||||
Expect(err).To(MatchError("Cover not found"))
|
||||
@@ -68,7 +68,7 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
|
||||
It("should fail when there is an unknown error", func() {
|
||||
cover.err = errors.New("weird error")
|
||||
r := newTestRequest("id=34", "size=128")
|
||||
r := newGetRequest("id=34", "size=128")
|
||||
_, err := controller.GetCoverArt(w, r)
|
||||
|
||||
Expect(err).To(MatchError("Internal Error"))
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/deluan/navidrome/engine"
|
||||
"github.com/deluan/navidrome/log"
|
||||
@@ -11,6 +13,24 @@ import (
|
||||
"github.com/deluan/navidrome/server/subsonic/responses"
|
||||
)
|
||||
|
||||
func postFormToQueryParams(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
SendError(w, r, NewError(responses.ErrorGeneric, err.Error()))
|
||||
}
|
||||
var parts []string
|
||||
for key, values := range r.Form {
|
||||
for _, v := range values {
|
||||
parts = append(parts, url.QueryEscape(key)+"="+url.QueryEscape(v))
|
||||
}
|
||||
}
|
||||
r.URL.RawQuery = strings.Join(parts, "&")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func checkRequiredParameters(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requiredParameters := []string{"u", "v", "c"}
|
||||
|
||||
@@ -13,8 +13,18 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func newTestRequest(queryParams ...string) *http.Request {
|
||||
r := httptest.NewRequest("get", "/ping?"+strings.Join(queryParams, "&"), nil)
|
||||
func newGetRequest(queryParams ...string) *http.Request {
|
||||
r := httptest.NewRequest("GET", "/ping?"+strings.Join(queryParams, "&"), nil)
|
||||
ctx := r.Context()
|
||||
return r.WithContext(log.NewContext(ctx))
|
||||
}
|
||||
|
||||
func newPostRequest(queryParam string, formFields ...string) *http.Request {
|
||||
r, err := http.NewRequest("POST", "/ping?"+queryParam, strings.NewReader(strings.Join(formFields, "&")))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
|
||||
ctx := r.Context()
|
||||
return r.WithContext(log.NewContext(ctx))
|
||||
}
|
||||
@@ -28,9 +38,37 @@ var _ = Describe("Middlewares", func() {
|
||||
w = httptest.NewRecorder()
|
||||
})
|
||||
|
||||
Describe("ParsePostForm", func() {
|
||||
It("converts any filed in a x-www-form-urlencoded POST into query params", func() {
|
||||
r := newPostRequest("a=abc", "u=user", "v=1.15", "c=test")
|
||||
cp := postFormToQueryParams(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(next.req.URL.Query().Get("a")).To(Equal("abc"))
|
||||
Expect(next.req.URL.Query().Get("u")).To(Equal("user"))
|
||||
Expect(next.req.URL.Query().Get("v")).To(Equal("1.15"))
|
||||
Expect(next.req.URL.Query().Get("c")).To(Equal("test"))
|
||||
})
|
||||
It("adds repeated params", func() {
|
||||
r := newPostRequest("a=abc", "id=1", "id=2")
|
||||
cp := postFormToQueryParams(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(next.req.URL.Query().Get("a")).To(Equal("abc"))
|
||||
Expect(next.req.URL.Query()["id"]).To(ConsistOf("1", "2"))
|
||||
})
|
||||
It("overrides query params with same key", func() {
|
||||
r := newPostRequest("a=query", "a=body")
|
||||
cp := postFormToQueryParams(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(next.req.URL.Query().Get("a")).To(Equal("body"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("CheckParams", func() {
|
||||
It("passes when all required params are available", func() {
|
||||
r := newTestRequest("u=user", "v=1.15", "c=test")
|
||||
r := newGetRequest("u=user", "v=1.15", "c=test")
|
||||
cp := checkRequiredParameters(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
@@ -41,7 +79,7 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
It("fails when user is missing", func() {
|
||||
r := newTestRequest("v=1.15", "c=test")
|
||||
r := newGetRequest("v=1.15", "c=test")
|
||||
cp := checkRequiredParameters(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
@@ -50,7 +88,7 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
It("fails when version is missing", func() {
|
||||
r := newTestRequest("u=user", "c=test")
|
||||
r := newGetRequest("u=user", "c=test")
|
||||
cp := checkRequiredParameters(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
@@ -59,7 +97,7 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
It("fails when client is missing", func() {
|
||||
r := newTestRequest("u=user", "v=1.15")
|
||||
r := newGetRequest("u=user", "v=1.15")
|
||||
cp := checkRequiredParameters(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
@@ -75,7 +113,7 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
It("passes all parameters to users.Authenticate ", func() {
|
||||
r := newTestRequest("u=valid", "p=password", "t=token", "s=salt")
|
||||
r := newGetRequest("u=valid", "p=password", "t=token", "s=salt")
|
||||
cp := authenticate(mockedUser)(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
@@ -89,7 +127,7 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
It("fails authentication with wrong password", func() {
|
||||
r := newTestRequest("u=invalid", "", "", "")
|
||||
r := newGetRequest("u=invalid", "", "", "")
|
||||
cp := authenticate(mockedUser)(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","albumList":{"album":[{"id":"1","isDir":false,"title":"title"}]}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","albumList":{"album":[{"id":"1","isDir":false,"title":"title"}]}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><albumList><album id="1" isDir="false" title="title"></album></albumList></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><albumList><album id="1" isDir="false" title="title"></album></albumList></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","albumList":{}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","albumList":{}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><albumList></albumList></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><albumList></albumList></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","directory":{"child":[{"id":"1","isDir":true,"title":"title","album":"album","artist":"artist","track":1,"year":1985,"genre":"Rock","coverArt":"1","size":"8421341","contentType":"audio/flac","suffix":"flac","starred":"2016-03-02T20:30:00Z","transcodedContentType":"audio/mpeg","transcodedSuffix":"mp3","duration":146,"bitRate":320}],"id":"1","name":"N"}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","directory":{"child":[{"id":"1","isDir":true,"title":"title","album":"album","artist":"artist","track":1,"year":1985,"genre":"Rock","coverArt":"1","size":"8421341","contentType":"audio/flac","suffix":"flac","starred":"2016-03-02T20:30:00Z","transcodedContentType":"audio/mpeg","transcodedSuffix":"mp3","duration":146,"bitRate":320}],"id":"1","name":"N"}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><directory id="1" name="N"><child id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320"></child></directory></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><directory id="1" name="N"><child id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320"></child></directory></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","directory":{"child":[{"id":"1","isDir":false,"title":"title"}],"id":"1","name":"N"}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","directory":{"child":[{"id":"1","isDir":false,"title":"title"}],"id":"1","name":"N"}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><directory id="1" name="N"><child id="1" isDir="false" title="title"></child></directory></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><directory id="1" name="N"><child id="1" isDir="false" title="title"></child></directory></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","directory":{"id":"1","name":"N"}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","directory":{"id":"1","name":"N"}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><directory id="1" name="N"></directory></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><directory id="1" name="N"></directory></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0"}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","genres":{"genre":[{"value":"Rock","songCount":1000,"albumCount":100},{"value":"Reggae","songCount":500,"albumCount":50},{"value":"Pop","songCount":0,"albumCount":0}]}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","genres":{"genre":[{"value":"Rock","songCount":1000,"albumCount":100},{"value":"Reggae","songCount":500,"albumCount":50},{"value":"Pop","songCount":0,"albumCount":0}]}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><genres><genre songCount="1000" albumCount="100">Rock</genre><genre songCount="500" albumCount="50">Reggae</genre><genre songCount="0" albumCount="0">Pop</genre></genres></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><genres><genre songCount="1000" albumCount="100">Rock</genre><genre songCount="500" albumCount="50">Reggae</genre><genre songCount="0" albumCount="0">Pop</genre></genres></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","genres":{}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","genres":{}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><genres></genres></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><genres></genres></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","indexes":{"index":[{"name":"A","artist":[{"id":"111","name":"aaa","starred":"2016-03-02T20:30:00Z"}]}],"lastModified":"1","ignoredArticles":"A"}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","indexes":{"index":[{"name":"A","artist":[{"id":"111","name":"aaa","starred":"2016-03-02T20:30:00Z"}]}],"lastModified":"1","ignoredArticles":"A"}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><indexes lastModified="1" ignoredArticles="A"><index name="A"><artist id="111" name="aaa" starred="2016-03-02T20:30:00Z"></artist></index></indexes></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><indexes lastModified="1" ignoredArticles="A"><index name="A"><artist id="111" name="aaa" starred="2016-03-02T20:30:00Z"></artist></index></indexes></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","indexes":{"lastModified":"1","ignoredArticles":"A"}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","indexes":{"lastModified":"1","ignoredArticles":"A"}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><indexes lastModified="1" ignoredArticles="A"></indexes></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><indexes lastModified="1" ignoredArticles="A"></indexes></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","license":{"valid":true}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","license":{"valid":true}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><license valid="true"></license></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><license valid="true"></license></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","musicFolders":{"musicFolder":[{"id":"111","name":"aaa"},{"id":"222","name":"bbb"}]}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","musicFolders":{"musicFolder":[{"id":"111","name":"aaa"},{"id":"222","name":"bbb"}]}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><musicFolders><musicFolder id="111" name="aaa"></musicFolder><musicFolder id="222" name="bbb"></musicFolder></musicFolders></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><musicFolders><musicFolder id="111" name="aaa"></musicFolder><musicFolder id="222" name="bbb"></musicFolder></musicFolders></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","musicFolders":{}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","musicFolders":{}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><musicFolders></musicFolders></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><musicFolders></musicFolders></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","playlists":{"playlist":[{"id":"111","name":"aaa"},{"id":"222","name":"bbb"}]}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","playlists":{"playlist":[{"id":"111","name":"aaa"},{"id":"222","name":"bbb"}]}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><playlists><playlist id="111" name="aaa"></playlist><playlist id="222" name="bbb"></playlist></playlists></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><playlists><playlist id="111" name="aaa"></playlist><playlist id="222" name="bbb"></playlist></playlists></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","playlists":{}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","playlists":{}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><playlists></playlists></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><playlists></playlists></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","user":{"username":"deluan","email":"navidrome@deluan.com","scrobblingEnabled":false,"adminRole":false,"settingsRole":false,"downloadRole":false,"uploadRole":false,"playlistRole":false,"coverArtRole":false,"commentRole":false,"podcastRole":false,"streamRole":false,"jukeboxRole":false,"shareRole":false,"videoConversionRole":false,"folder":[1]}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","user":{"username":"deluan","email":"navidrome@deluan.com","scrobblingEnabled":false,"adminRole":false,"settingsRole":false,"downloadRole":false,"uploadRole":false,"playlistRole":false,"coverArtRole":false,"commentRole":false,"podcastRole":false,"streamRole":false,"jukeboxRole":false,"shareRole":false,"videoConversionRole":false,"folder":[1]}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><user username="deluan" email="navidrome@deluan.com" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false"><folder>1</folder></user></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><user username="deluan" email="navidrome@deluan.com" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false"><folder>1</folder></user></subsonic-response>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"ok","version":"1.8.0","user":{"username":"deluan","scrobblingEnabled":false,"adminRole":false,"settingsRole":false,"downloadRole":false,"uploadRole":false,"playlistRole":false,"coverArtRole":false,"commentRole":false,"podcastRole":false,"streamRole":false,"jukeboxRole":false,"shareRole":false,"videoConversionRole":false}}
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","user":{"username":"deluan","scrobblingEnabled":false,"adminRole":false,"settingsRole":false,"downloadRole":false,"uploadRole":false,"playlistRole":false,"coverArtRole":false,"commentRole":false,"podcastRole":false,"streamRole":false,"jukeboxRole":false,"shareRole":false,"videoConversionRole":false}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0"><user username="deluan" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false"></user></subsonic-response>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><user username="deluan" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false"></user></subsonic-response>
|
||||
|
||||
@@ -9,6 +9,8 @@ type Subsonic struct {
|
||||
XMLName xml.Name `xml:"http://subsonic.org/restapi subsonic-response" json:"-"`
|
||||
Status string `xml:"status,attr" json:"status"`
|
||||
Version string `xml:"version,attr" json:"version"`
|
||||
Type string `xml:"type,attr" json:"type"`
|
||||
ServerVersion string `xml:"serverVersion,attr" json:"serverVersion"`
|
||||
Error *Error `xml:"error,omitempty" json:"error,omitempty"`
|
||||
License *License `xml:"license,omitempty" json:"license,omitempty"`
|
||||
MusicFolders *MusicFolders `xml:"musicFolders,omitempty" json:"musicFolders,omitempty"`
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/xml"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/navidrome/consts"
|
||||
. "github.com/deluan/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
var _ = Describe("Responses", func() {
|
||||
var response *Subsonic
|
||||
BeforeEach(func() {
|
||||
response = &Subsonic{Status: "ok", Version: "1.8.0"}
|
||||
response = &Subsonic{Status: "ok", Version: "1.8.0", Type: consts.AppName, ServerVersion: "v0.0.0"}
|
||||
})
|
||||
|
||||
Describe("EmptyResponse", func() {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
content="Navidrome Music Server"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
|
||||
@@ -19,11 +19,11 @@ const UserEdit = (props) => (
|
||||
<SimpleForm>
|
||||
<TextInput source="userName" validate={[required()]} />
|
||||
<TextInput source="name" validate={[required()]} />
|
||||
<TextInput source="email" validate={[required(), email()]} />
|
||||
<TextInput source="email" validate={[email()]} />
|
||||
<PasswordInput source="password" validate={[required()]} />
|
||||
<BooleanInput source="isAdmin" initialValue={false} />
|
||||
<DateField source="lastLoginAt" showTime />
|
||||
<DateField source="lastAccessAt" showTime />
|
||||
{/*<DateField source="lastAccessAt" showTime />*/}
|
||||
<DateField source="updatedAt" showTime />
|
||||
<DateField source="createdAt" showTime />
|
||||
</SimpleForm>
|
||||
|
||||
@@ -33,8 +33,7 @@ const UserList = (props) => {
|
||||
<SimpleList
|
||||
primaryText={(record) => record.name}
|
||||
secondaryText={(record) =>
|
||||
record.lastAccessAt &&
|
||||
new Date(record.lastAccessAt).toLocaleString()
|
||||
record.lastLoginAt && new Date(record.lastLoginAt).toLocaleString()
|
||||
}
|
||||
tertiaryText={(record) => (record.isAdmin ? '[admin]️' : '')}
|
||||
/>
|
||||
@@ -42,7 +41,7 @@ const UserList = (props) => {
|
||||
<Datagrid rowClick="edit">
|
||||
<TextField source="userName" />
|
||||
<BooleanField source="isAdmin" />
|
||||
<DateField source="lastAccessAt" locales="pt-BR" />
|
||||
<DateField source="lastLoginAt" locales="pt-BR" />
|
||||
<DateField source="updatedAt" locales="pt-BR" />
|
||||
</Datagrid>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user