Compare commits

...

23 Commits

Author SHA1 Message Date
Deluan
c9f5625abf fix: skip files with errors during scan 2020-02-01 11:25:31 -05:00
Deluan
22d57a7c26 chore: go mod tidy 2020-01-30 16:36:43 -05:00
Deluan
0c5bf18d80 build: add release and dist targets 2020-01-30 16:33:27 -05:00
Deluan
9b7d1757e7 build: add goose to setup target, add dist target 2020-01-30 16:08:39 -05:00
Deluan
c34a5dcb07 docs: update README 2020-01-30 16:07:54 -05:00
Deluan
90a1e6d213 feat: add server name and version to all responses
This is inline with other Subsonic compatible servers, like funkwhale, madsonic, ampache...
2020-01-30 14:43:24 -05:00
Deluan
482350c076 build: run tests in Dockerfile 2020-01-29 17:09:46 -05:00
Deluan
64388b2d4a fix: correct description meta in index.html 2020-01-29 16:56:22 -05:00
Deluan
3007ca68d5 fix: disable User.lastAccessAt field for now.
Updating it on every request was cause DB retentions/lock errors
2020-01-28 16:20:59 -05:00
Deluan
d4edff3aaa fix: only add the latest tag to version if the tag is attached to the current commit, or else use the branch name 2020-01-28 15:28:39 -05:00
Deluan
99b1dc1421 feat: upgrade ffmpeg in docker image 2020-01-28 15:01:23 -05:00
dependabot-preview[bot]
37dfe4c092 Bump github.com/mattn/go-sqlite3
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 2.0.2+incompatible to 2.0.3+incompatible.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v2.0.2...v2.0.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-28 07:37:40 -05:00
Deluan
1278863416 feat: support clients that send the API params as a x-www-form-urlencoded POST 2020-01-27 15:10:46 -05:00
Deluan
cffae3a7d6 ci: don't add 'ci:' commits to the changelog 2020-01-27 09:44:28 -05:00
Deluan
0d2911daf9 refactor: add Context to the persistence layer 2020-01-27 09:41:33 -05:00
Deluan
3c54b776d6 docs: add Discord invite 2020-01-27 08:05:15 -05:00
Deluan Quintão
34127805ce doc: update compatibility table 2020-01-27 04:32:55 -05:00
Deluan
ac4aa1ebe2 feat: PORT env var can override configured port 2020-01-26 22:18:30 -05:00
Deluan
b7d7251cf4 fix: user's email is not mandatory 2020-01-26 22:17:58 -05:00
Deluan
7bf110f22f docs: update readme 2020-01-26 21:54:49 -05:00
Deluan
3b651a3728 docs: update readme 2020-01-26 21:34:53 -05:00
Deluan
13390c2edb fix: add git sha and tag to built image 2020-01-26 20:52:09 -05:00
Deluan
221d320ccf docs: add latest released version to README 2020-01-26 20:51:41 -05:00
75 changed files with 388 additions and 274 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,17 @@
# Navidrome Music Streamer
[![Build Status](https://github.com/deluan/navidrome/workflows/Build/badge.svg)](https://github.com/deluan/navidrome/actions)
[![Build](https://img.shields.io/github/workflow/status/deluan/navidrome/Build?style=for-the-badge)](https://github.com/deluan/navidrome/actions)
[![Last Release](https://img.shields.io/github/v/release/deluan/navidrome?label=latest&style=for-the-badge)](https://github.com/deluan/navidrome/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/deluan/navidrome?style=for-the-badge)](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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"}]}}

View File

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

View File

@@ -1 +1 @@
{"status":"ok","version":"1.8.0","albumList":{}}
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","albumList":{}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
{"status":"ok","version":"1.8.0"}
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0"}

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
{"status":"ok","version":"1.8.0","genres":{}}
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","genres":{}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"}]}}

View File

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

View File

@@ -1 +1 @@
{"status":"ok","version":"1.8.0","musicFolders":{}}
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","musicFolders":{}}

View File

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

View File

@@ -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"}]}}

View File

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

View File

@@ -1 +1 @@
{"status":"ok","version":"1.8.0","playlists":{}}
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","playlists":{}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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" />
<!--

View File

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

View File

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