mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Merge branch 'master' of https://github.com/deluan/navidrome into gridplay-feature
This commit is contained in:
commit
636c4f3740
27
.github/workflows/pipeline.yml
vendored
27
.github/workflows/pipeline.yml
vendored
@ -9,6 +9,20 @@ on:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
golangci-lint:
|
||||
name: Lint Server
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: actions-contrib/golangci-lint@v1
|
||||
with:
|
||||
golangci_lint_version: v1.25.0
|
||||
# TODO Enable github actions output format: https://github.com/actions-contrib/golangci-lint/issues/11
|
||||
# args: run --out-format github-actions
|
||||
|
||||
go:
|
||||
name: Test Server on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
@ -64,6 +78,11 @@ jobs:
|
||||
cd ui
|
||||
npm ci
|
||||
|
||||
- name: npm check-formatting
|
||||
run: |
|
||||
cd ui
|
||||
npm run check-formatting
|
||||
|
||||
- name: npm build
|
||||
run: |
|
||||
cd ui
|
||||
@ -76,7 +95,7 @@ jobs:
|
||||
|
||||
binaries:
|
||||
name: Binaries
|
||||
needs: [js, go]
|
||||
needs: [js, go, golangci-lint]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@ -115,21 +134,27 @@ jobs:
|
||||
name: Docker images
|
||||
needs: [binaries]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_IMAGE: ${{secrets.DOCKER_IMAGE}}
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
if: ${{env.DOCKER_IMAGE}} != ''
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- uses: actions/checkout@v1
|
||||
if: ${{env.DOCKER_IMAGE}} != ''
|
||||
|
||||
- uses: actions/download-artifact@v1
|
||||
if: ${{env.DOCKER_IMAGE}} != ''
|
||||
with:
|
||||
name: binaries
|
||||
path: dist
|
||||
|
||||
- name: Build the Docker image and push
|
||||
if: ${{env.DOCKER_IMAGE}} != ''
|
||||
env:
|
||||
DOCKER_IMAGE: ${{secrets.DOCKER_IMAGE}}
|
||||
DOCKER_PLATFORM: linux/amd64,linux/arm/v7,linux/arm64
|
||||
|
||||
11
.golangci.yml
Normal file
11
.golangci.yml
Normal file
@ -0,0 +1,11 @@
|
||||
linters:
|
||||
enable:
|
||||
- goimports
|
||||
- unconvert
|
||||
- gosec
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- linters:
|
||||
- gosec
|
||||
text: "(G501|G401):"
|
||||
7
Makefile
7
Makefile
@ -35,15 +35,18 @@ testall: check_go_env test
|
||||
.PHONY: testall
|
||||
|
||||
setup:
|
||||
@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)
|
||||
go mod download
|
||||
.PHONY: setup
|
||||
|
||||
setup-dev: setup
|
||||
@which wire || (echo "Installing Wire" && GO111MODULE=off go get -u github.com/google/wire/cmd/wire)
|
||||
@which ginkgo || (echo "Installing Ginkgo" && GO111MODULE=off go get -u github.com/onsi/ginkgo/ginkgo)
|
||||
@which goose || (echo "Installing Goose" && GO111MODULE=off go get -u github.com/pressly/goose/cmd/goose)
|
||||
@which lefthook || (echo "Installing Lefthook" && GO111MODULE=off go get -u github.com/Arkweid/lefthook)
|
||||
@lefthook install
|
||||
go mod download
|
||||
@(cd ./ui && npm ci)
|
||||
.PHONY: setup
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
@ -22,7 +23,7 @@ var (
|
||||
|
||||
func InitTokenAuth(ds model.DataStore) {
|
||||
once.Do(func() {
|
||||
secret, err := ds.Property(nil).DefaultGet(consts.JWTSecretKey, "not so secret")
|
||||
secret, err := ds.Property(context.TODO()).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)
|
||||
}
|
||||
|
||||
@ -80,10 +80,6 @@ func (b *browser) Artist(ctx context.Context, id string) (*DirectoryInfo, error)
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(ctx, "Found Artist", "id", id, "name", a.Name)
|
||||
var albumIds []string
|
||||
for _, al := range albums {
|
||||
albumIds = append(albumIds, al.ID)
|
||||
}
|
||||
return b.buildArtistDir(a, albums), nil
|
||||
}
|
||||
|
||||
@ -93,11 +89,6 @@ func (b *browser) Album(ctx context.Context, id string) (*DirectoryInfo, error)
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(ctx, "Found Album", "id", id, "name", al.Name)
|
||||
var mfIds []string
|
||||
for _, mf := range tracks {
|
||||
mfIds = append(mfIds, mf.ID)
|
||||
}
|
||||
|
||||
return b.buildAlbumDir(al, tracks), nil
|
||||
}
|
||||
|
||||
|
||||
@ -74,7 +74,9 @@ func (c *cover) Get(ctx context.Context, id string, size int, out io.Writer) err
|
||||
log.Error(ctx, "Error loading cover art", "path", path, "size", size, err)
|
||||
return
|
||||
}
|
||||
io.Copy(w, reader)
|
||||
if _, err := io.Copy(w, reader); err != nil {
|
||||
log.Error(ctx, "Error saving covert art to cache", "path", path, "size", size, err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
log.Trace(ctx, "Loading image from cache", "path", path, "size", size, "lastUpdate", lastUpdate)
|
||||
|
||||
@ -2,6 +2,7 @@ package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
|
||||
"github.com/deluan/navidrome/log"
|
||||
@ -14,7 +15,7 @@ import (
|
||||
var _ = Describe("Cover", func() {
|
||||
var cover Cover
|
||||
var ds model.DataStore
|
||||
ctx := log.NewContext(nil)
|
||||
ctx := log.NewContext(context.TODO())
|
||||
|
||||
BeforeEach(func() {
|
||||
ds = &persistence.MockDataStore{MockedTranscoding: &mockTranscodingRepository{}}
|
||||
|
||||
@ -106,18 +106,6 @@ type listGenerator struct {
|
||||
npRepo NowPlayingRepository
|
||||
}
|
||||
|
||||
func (g *listGenerator) query(ctx context.Context, qo model.QueryOptions) (Entries, error) {
|
||||
albums, err := g.ds.Album(ctx).GetAll(qo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
albumIds := make([]string, len(albums))
|
||||
for i, al := range albums {
|
||||
albumIds[i] = al.ID
|
||||
}
|
||||
return FromAlbums(albums), err
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetSongs(ctx context.Context, offset, size int, filter ListFilter) (Entries, error) {
|
||||
qo := model.QueryOptions(filter)
|
||||
qo.Offset = offset
|
||||
@ -170,16 +158,6 @@ func (g *listGenerator) GetAllStarred(ctx context.Context) (artists Entries, alb
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
var mfIds []string
|
||||
for _, mf := range mfs {
|
||||
mfIds = append(mfIds, mf.ID)
|
||||
}
|
||||
|
||||
var artistIds []string
|
||||
for _, ar := range ars {
|
||||
artistIds = append(artistIds, ar.ID)
|
||||
}
|
||||
|
||||
artists = FromArtists(ars)
|
||||
albums = FromAlbums(als)
|
||||
mediaFiles = FromMediaFiles(mfs)
|
||||
@ -200,7 +178,7 @@ func (g *listGenerator) GetNowPlaying(ctx context.Context) (Entries, error) {
|
||||
}
|
||||
entries[i] = FromMediaFile(mf)
|
||||
entries[i].UserName = np.Username
|
||||
entries[i].MinutesAgo = int(time.Now().Sub(np.Start).Minutes())
|
||||
entries[i].MinutesAgo = int(time.Since(np.Start).Minutes())
|
||||
entries[i].PlayerId = np.PlayerId
|
||||
entries[i].PlayerName = np.PlayerName
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ var _ = Describe("MediaStreamer", func() {
|
||||
var streamer MediaStreamer
|
||||
var ds model.DataStore
|
||||
ffmpeg := &fakeFFmpeg{Data: "fake data"}
|
||||
ctx := log.NewContext(nil)
|
||||
ctx := log.NewContext(context.TODO())
|
||||
|
||||
BeforeEach(func() {
|
||||
ds = &persistence.MockDataStore{MockedTranscoding: &mockTranscodingRepository{}}
|
||||
|
||||
@ -110,7 +110,7 @@ func checkExpired(l *list.List, f func() *list.Element) *list.Element {
|
||||
return nil
|
||||
}
|
||||
start := e.Value.(*NowPlayingInfo).Start
|
||||
if time.Now().Sub(start) < NowPlayingExpire {
|
||||
if time.Since(start) < NowPlayingExpire {
|
||||
return e
|
||||
}
|
||||
l.Remove(e)
|
||||
|
||||
@ -14,7 +14,7 @@ import (
|
||||
var _ = Describe("Players", func() {
|
||||
var players Players
|
||||
var repo *mockPlayerRepository
|
||||
ctx := context.WithValue(log.NewContext(nil), "user", model.User{ID: "userid", UserName: "johndoe"})
|
||||
ctx := context.WithValue(log.NewContext(context.TODO()), "user", model.User{ID: "userid", UserName: "johndoe"})
|
||||
ctx = context.WithValue(ctx, "username", "johndoe")
|
||||
var beforeRegister time.Time
|
||||
|
||||
|
||||
@ -69,7 +69,7 @@ func (p *playlists) Delete(ctx context.Context, playlistId string) error {
|
||||
if owner != pls.Owner {
|
||||
return model.ErrNotAuthorized
|
||||
}
|
||||
return p.ds.Playlist(nil).Delete(playlistId)
|
||||
return p.ds.Playlist(ctx).Delete(playlistId)
|
||||
}
|
||||
|
||||
func (p *playlists) Update(ctx context.Context, playlistId string, name *string, idsToAdd []string, idxToRemove []int) error {
|
||||
|
||||
@ -2,7 +2,6 @@ package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@ -57,7 +56,7 @@ func (s *scrobbler) NowPlaying(ctx context.Context, playerId int, playerName, tr
|
||||
}
|
||||
|
||||
if mf == nil {
|
||||
return nil, errors.New(fmt.Sprintf(`ID "%s" not found`, trackId))
|
||||
return nil, fmt.Errorf(`ID "%s" not found`, trackId)
|
||||
}
|
||||
|
||||
log.Info("Now Playing", "title", mf.Title, "artist", mf.Artist, "user", userName(ctx))
|
||||
|
||||
@ -30,7 +30,7 @@ func (ff *ffmpeg) Start(ctx context.Context, command, path string, maxBitRate in
|
||||
args := createTranscodeCommand(command, path, maxBitRate)
|
||||
|
||||
log.Trace(ctx, "Executing ffmpeg command", "cmd", args)
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd := exec.Command(args[0], args[1:]...) // #nosec
|
||||
cmd.Stderr = os.Stderr
|
||||
if f, err = cmd.StdoutPipe(); err != nil {
|
||||
return
|
||||
@ -38,7 +38,9 @@ func (ff *ffmpeg) Start(ctx context.Context, command, path string, maxBitRate in
|
||||
if err = cmd.Start(); err != nil {
|
||||
return
|
||||
}
|
||||
go cmd.Wait() // prevent zombies
|
||||
|
||||
go func() { _ = cmd.Wait() }() // prevent zombies
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -180,6 +180,7 @@ func extractLogger(ctx interface{}) (*logrus.Entry, error) {
|
||||
if logger != nil {
|
||||
return logger.(*logrus.Entry), nil
|
||||
}
|
||||
return extractLogger(NewContext(ctx))
|
||||
case *http.Request:
|
||||
return extractLogger(ctx.Context())
|
||||
}
|
||||
|
||||
@ -41,8 +41,8 @@ var _ = Describe("Logger", func() {
|
||||
Expect(hook.LastEntry().Data).To(BeEmpty())
|
||||
})
|
||||
|
||||
XIt("Empty context", func() {
|
||||
Error(context.Background(), "Simple Message")
|
||||
It("Empty context", func() {
|
||||
Error(context.TODO(), "Simple Message")
|
||||
Expect(hook.LastEntry().Message).To(Equal("Simple Message"))
|
||||
Expect(hook.LastEntry().Data).To(BeEmpty())
|
||||
})
|
||||
@ -70,7 +70,7 @@ var _ = Describe("Logger", func() {
|
||||
})
|
||||
|
||||
It("can get data from the request's context", func() {
|
||||
ctx := NewContext(nil, "foo", "bar")
|
||||
ctx := NewContext(context.TODO(), "foo", "bar")
|
||||
req := httptest.NewRequest("get", "/", nil).WithContext(ctx)
|
||||
|
||||
Error(req, "Simple Message", "key1", "value1")
|
||||
|
||||
@ -14,7 +14,7 @@ var _ = Describe("AlbumRepository", func() {
|
||||
var repo model.AlbumRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx := context.WithValue(log.NewContext(nil), "user", model.User{ID: "userid"})
|
||||
ctx := context.WithValue(log.NewContext(context.TODO()), "user", model.User{ID: "userid"})
|
||||
repo = NewAlbumRepository(ctx, orm.NewOrm())
|
||||
})
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ var _ = Describe("ArtistRepository", func() {
|
||||
var repo model.ArtistRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx := context.WithValue(log.NewContext(nil), "user", model.User{ID: "userid"})
|
||||
ctx := context.WithValue(log.NewContext(context.TODO()), "user", model.User{ID: "userid"})
|
||||
repo = NewArtistRepository(ctx, orm.NewOrm())
|
||||
})
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package persistence_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
@ -13,7 +15,7 @@ var _ = Describe("GenreRepository", func() {
|
||||
var repo model.GenreRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
repo = persistence.NewGenreRepository(log.NewContext(nil), orm.NewOrm())
|
||||
repo = persistence.NewGenreRepository(log.NewContext(context.TODO()), orm.NewOrm())
|
||||
})
|
||||
|
||||
It("returns all records", func() {
|
||||
|
||||
@ -16,7 +16,7 @@ var _ = Describe("MediaRepository", func() {
|
||||
var mr model.MediaFileRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx := context.WithValue(log.NewContext(nil), "user", model.User{ID: "userid"})
|
||||
ctx := context.WithValue(log.NewContext(context.TODO()), "user", model.User{ID: "userid"})
|
||||
mr = NewMediaFileRepository(ctx, orm.NewOrm())
|
||||
})
|
||||
|
||||
|
||||
@ -22,8 +22,7 @@ func TestPersistence(t *testing.T) {
|
||||
//os.Remove("./test-123.db")
|
||||
//conf.Server.Path = "./test-123.db"
|
||||
conf.Server.DbPath = "file::memory:?cache=shared"
|
||||
orm.RegisterDataBase("default", db.Driver, conf.Server.DbPath)
|
||||
New()
|
||||
_ = orm.RegisterDataBase("default", db.Driver, conf.Server.DbPath)
|
||||
db.EnsureLatestVersion()
|
||||
log.SetLevel(log.LevelCritical)
|
||||
RegisterFailHandler(Fail)
|
||||
@ -85,7 +84,7 @@ var _ = Describe("Initialize test DB", func() {
|
||||
// TODO Load this data setup from file(s)
|
||||
BeforeSuite(func() {
|
||||
o := orm.NewOrm()
|
||||
ctx := context.WithValue(log.NewContext(nil), "user", model.User{ID: "userid"})
|
||||
ctx := context.WithValue(log.NewContext(context.TODO()), "user", model.User{ID: "userid"})
|
||||
mr := NewMediaFileRepository(ctx, o)
|
||||
for _, s := range testSongs {
|
||||
err := mr.Put(&s)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
@ -12,7 +14,7 @@ var _ = Describe("PlaylistRepository", func() {
|
||||
var repo model.PlaylistRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
repo = NewPlaylistRepository(log.NewContext(nil), orm.NewOrm())
|
||||
repo = NewPlaylistRepository(log.NewContext(context.TODO()), orm.NewOrm())
|
||||
})
|
||||
|
||||
Describe("Count", func() {
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
. "github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
@ -12,7 +14,7 @@ var _ = Describe("Property Repository", func() {
|
||||
var pr model.PropertyRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
pr = NewPropertyRepository(NewContext(nil), orm.NewOrm())
|
||||
pr = NewPropertyRepository(log.NewContext(context.TODO()), orm.NewOrm())
|
||||
})
|
||||
|
||||
It("saves and restore a new property", func() {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
@ -12,7 +14,7 @@ var _ = Describe("UserRepository", func() {
|
||||
var repo model.UserRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
repo = NewUserRepository(log.NewContext(nil), orm.NewOrm())
|
||||
repo = NewUserRepository(log.NewContext(context.TODO()), orm.NewOrm())
|
||||
})
|
||||
|
||||
Describe("Put/Get/FindByUsername", func() {
|
||||
|
||||
@ -103,8 +103,8 @@ var _ = Describe("ChangeDetector", func() {
|
||||
Expect(changed).To(BeEmpty())
|
||||
Expect(changed).To(BeEmpty())
|
||||
|
||||
f, err := os.Create(filepath.Join(testFolder, "a", "b", "new.txt"))
|
||||
f.Close()
|
||||
f, _ := os.Create(filepath.Join(testFolder, "a", "b", "new.txt"))
|
||||
_ = f.Close()
|
||||
changed, deleted, err = newScanner.Scan(lastModifiedSince)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(deleted).To(BeEmpty())
|
||||
|
||||
@ -84,7 +84,7 @@ func ExtractAllMetadata(inputs []string) (map[string]*Metadata, error) {
|
||||
args := createProbeCommand(inputs)
|
||||
|
||||
log.Trace("Executing command", "args", args)
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd := exec.Command(args[0], args[1:]...) // #nosec
|
||||
output, _ := cmd.CombinedOutput()
|
||||
mds := map[string]*Metadata{}
|
||||
if len(output) == 0 {
|
||||
|
||||
@ -34,7 +34,7 @@ func (s *Scanner) Rescan(mediaFolder string, fullRescan bool) error {
|
||||
log.Debug("Scanning folder (full scan)", "folder", mediaFolder)
|
||||
}
|
||||
|
||||
err := folderScanner.Scan(log.NewContext(nil), lastModifiedSince)
|
||||
err := folderScanner.Scan(log.NewContext(context.TODO()), lastModifiedSince)
|
||||
if err != nil {
|
||||
log.Error("Error importing MediaFolder", "folder", mediaFolder, err)
|
||||
}
|
||||
@ -59,7 +59,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(nil).Get(model.PropLastScan + "-" + folder)
|
||||
ms, err := s.ds.Property(context.TODO()).Get(model.PropLastScan + "-" + folder)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
@ -72,11 +72,13 @@ 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(nil).Put(model.PropLastScan+"-"+folder, fmt.Sprint(millis))
|
||||
if err := s.ds.Property(context.TODO()).Put(model.PropLastScan+"-"+folder, fmt.Sprint(millis)); err != nil {
|
||||
log.Error("Error updating DB after scan", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) loadFolders() {
|
||||
fs, _ := s.ds.MediaFolder(nil).GetAll()
|
||||
fs, _ := s.ds.MediaFolder(context.TODO()).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)
|
||||
@ -85,12 +87,6 @@ func (s *Scanner) loadFolders() {
|
||||
|
||||
type Status int
|
||||
|
||||
const (
|
||||
StatusComplete Status = iota
|
||||
StatusInProgress
|
||||
StatusError
|
||||
)
|
||||
|
||||
type StatusInfo struct {
|
||||
MediaFolder string
|
||||
Status Status
|
||||
|
||||
@ -113,7 +113,7 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time) erro
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ds.GC(log.NewContext(nil))
|
||||
err = s.ds.GC(log.NewContext(context.TODO()))
|
||||
log.Info("Finished Music Folder", "folder", s.rootFolder, "elapsed", time.Since(start))
|
||||
|
||||
return err
|
||||
|
||||
@ -49,7 +49,7 @@ func (app *Router) routes(path string) http.Handler {
|
||||
app.R(r, "/player", model.Player{})
|
||||
|
||||
// Keepalive endpoint to be used to keep the session valid (ex: while playing songs)
|
||||
r.Get("/keepalive/*", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"response":"ok"}`)) })
|
||||
r.Get("/keepalive/*", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"response":"ok"}`)) })
|
||||
})
|
||||
|
||||
// Serve UI app assets
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/navidrome/consts"
|
||||
@ -20,7 +19,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ErrFirstTime = errors.New("no users created")
|
||||
)
|
||||
|
||||
@ -31,7 +29,7 @@ func Login(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) {
|
||||
username, password, err := getCredentialsFromBody(r)
|
||||
if err != nil {
|
||||
log.Error(r, "Parsing request body", err)
|
||||
rest.RespondWithError(w, http.StatusUnprocessableEntity, err.Error())
|
||||
_ = rest.RespondWithError(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@ -42,21 +40,21 @@ 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(r.Context()), username, password)
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authentication user. Please try again")
|
||||
_ = rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authentication user. Please try again")
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
log.Warn(r, "Unsuccessful login", "username", username, "request", r.Header)
|
||||
rest.RespondWithError(w, http.StatusUnauthorized, "Invalid username or password")
|
||||
_ = rest.RespondWithError(w, http.StatusUnauthorized, "Invalid username or password")
|
||||
return
|
||||
}
|
||||
|
||||
tokenString, err := auth.CreateToken(user)
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authenticating user. Please try again")
|
||||
_ = rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authenticating user. Please try again")
|
||||
return
|
||||
}
|
||||
rest.RespondWithJSON(w, http.StatusOK,
|
||||
_ = rest.RespondWithJSON(w, http.StatusOK,
|
||||
map[string]interface{}{
|
||||
"message": "User '" + username + "' authenticated successfully",
|
||||
"token": tokenString,
|
||||
@ -71,7 +69,7 @@ func getCredentialsFromBody(r *http.Request) (username string, password string,
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err = decoder.Decode(&data); err != nil {
|
||||
log.Error(r, "parsing request body", err)
|
||||
err = errors.New("Invalid request payload")
|
||||
err = errors.New("invalid request payload")
|
||||
return
|
||||
}
|
||||
username = data["username"]
|
||||
@ -86,21 +84,21 @@ func CreateAdmin(ds model.DataStore) func(w http.ResponseWriter, r *http.Request
|
||||
username, password, err := getCredentialsFromBody(r)
|
||||
if err != nil {
|
||||
log.Error(r, "parsing request body", err)
|
||||
rest.RespondWithError(w, http.StatusUnprocessableEntity, err.Error())
|
||||
_ = rest.RespondWithError(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
c, err := ds.User(r.Context()).CountAll()
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
|
||||
_ = rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if c > 0 {
|
||||
rest.RespondWithError(w, http.StatusForbidden, "Cannot create another first admin")
|
||||
_ = rest.RespondWithError(w, http.StatusForbidden, "Cannot create another first admin")
|
||||
return
|
||||
}
|
||||
err = createDefaultUser(r.Context(), ds, username, password)
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
|
||||
_ = rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
handleLogin(ds, username, password, w, r)
|
||||
@ -186,11 +184,11 @@ func authenticator(ds model.DataStore) func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := getToken(ds, r.Context())
|
||||
if err == ErrFirstTime {
|
||||
rest.RespondWithJSON(w, http.StatusUnauthorized, map[string]string{"message": ErrFirstTime.Error()})
|
||||
_ = rest.RespondWithJSON(w, http.StatusUnauthorized, map[string]string{"message": ErrFirstTime.Error()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
|
||||
_ = rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
@ -200,7 +198,7 @@ func authenticator(ds model.DataStore) func(next http.Handler) http.Handler {
|
||||
newTokenString, err := auth.TouchToken(token)
|
||||
if err != nil {
|
||||
log.Error(r, "signing new token", err)
|
||||
rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
|
||||
_ = rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
@ -18,7 +19,8 @@ func initialSetup(ds model.DataStore) {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := ds.Property(nil).Get(consts.InitialSetupFlagKey)
|
||||
properties := ds.Property(context.TODO())
|
||||
_, err := properties.Get(consts.InitialSetupFlagKey)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
@ -33,13 +35,14 @@ func initialSetup(ds model.DataStore) {
|
||||
}
|
||||
}
|
||||
|
||||
err = ds.Property(nil).Put(consts.InitialSetupFlagKey, time.Now().String())
|
||||
err = properties.Put(consts.InitialSetupFlagKey, time.Now().String())
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func createInitialAdminUser(ds model.DataStore) error {
|
||||
c, err := ds.User(nil).CountAll()
|
||||
users := ds.User(context.TODO())
|
||||
c, err := users.CountAll()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Could not access User table: %s", err))
|
||||
}
|
||||
@ -59,7 +62,7 @@ func createInitialAdminUser(ds model.DataStore) error {
|
||||
Password: initialPassword,
|
||||
IsAdmin: true,
|
||||
}
|
||||
err := ds.User(nil).Put(&initialUser)
|
||||
err := users.Put(&initialUser)
|
||||
if err != nil {
|
||||
log.Error("Could not create initial admin user", "user", initialUser, err)
|
||||
}
|
||||
@ -68,13 +71,14 @@ func createInitialAdminUser(ds model.DataStore) error {
|
||||
}
|
||||
|
||||
func createJWTSecret(ds model.DataStore) error {
|
||||
_, err := ds.Property(nil).Get(consts.JWTSecretKey)
|
||||
properties := ds.Property(context.TODO())
|
||||
_, err := properties.Get(consts.JWTSecretKey)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
jwtSecret, _ := uuid.NewRandom()
|
||||
log.Warn("Creating JWT secret, used for encrypting UI sessions")
|
||||
err = ds.Property(nil).Put(consts.JWTSecretKey, jwtSecret.String())
|
||||
err = properties.Put(consts.JWTSecretKey, jwtSecret.String())
|
||||
if err != nil {
|
||||
log.Error("Could not save JWT secret in DB", err)
|
||||
}
|
||||
@ -82,8 +86,8 @@ func createJWTSecret(ds model.DataStore) error {
|
||||
}
|
||||
|
||||
func createDefaultTranscodings(ds model.DataStore) error {
|
||||
repo := ds.Transcoding(nil)
|
||||
c, _ := repo.CountAll()
|
||||
transcodings := ds.Transcoding(context.TODO())
|
||||
c, _ := transcodings.CountAll()
|
||||
if c != 0 {
|
||||
return nil
|
||||
}
|
||||
@ -98,7 +102,7 @@ func createDefaultTranscodings(ds model.DataStore) error {
|
||||
return err
|
||||
}
|
||||
log.Info("Creating default transcoding config", "name", t.Name)
|
||||
if err = repo.Put(&t); err != nil {
|
||||
if err = transcodings.Put(&t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +162,7 @@ func H(r chi.Router, path string, f Handler) {
|
||||
func HGone(r chi.Router, path string) {
|
||||
handle := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(410)
|
||||
w.Write([]byte("This endpoint will not be implemented"))
|
||||
_, _ = w.Write([]byte("This endpoint will not be implemented"))
|
||||
}
|
||||
r.HandleFunc("/"+path, handle)
|
||||
r.HandleFunc("/"+path+".view", handle)
|
||||
@ -207,5 +207,7 @@ func SendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Sub
|
||||
} else {
|
||||
log.Warn(r.Context(), "API: Failed response", "error", payload.Error.Code, "message", payload.Error.Message)
|
||||
}
|
||||
w.Write(response)
|
||||
if _, err := w.Write(response); err != nil {
|
||||
log.Error(r, "Error sending response to client", "payload", string(response), err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ func (c *MediaRetrievalController) GetAvatar(w http.ResponseWriter, r *http.Requ
|
||||
return nil, NewError(responses.ErrorDataNotFound, "Avatar image not found")
|
||||
}
|
||||
defer f.Close()
|
||||
io.Copy(w, f)
|
||||
_, _ = io.Copy(w, f)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -24,8 +24,8 @@ func (c *fakeCover) Get(ctx context.Context, id string, size int, out io.Writer)
|
||||
}
|
||||
c.recvId = id
|
||||
c.recvSize = size
|
||||
out.Write([]byte(c.data))
|
||||
return nil
|
||||
_, err := out.Write([]byte(c.data))
|
||||
return err
|
||||
}
|
||||
|
||||
var _ = Describe("MediaRetrievalController", func() {
|
||||
|
||||
@ -22,7 +22,7 @@ func Init(t *testing.T, skipOnShort bool) {
|
||||
appPath, _ := filepath.Abs(filepath.Join(filepath.Dir(file), ".."))
|
||||
confPath, _ := filepath.Abs(filepath.Join(appPath, "tests", "navidrome-test.toml"))
|
||||
println("Loading test configuration file from " + confPath)
|
||||
os.Chdir(appPath)
|
||||
_ = os.Chdir(appPath)
|
||||
conf.LoadFromFile("tests/navidrome-test.toml", true)
|
||||
|
||||
noLog := os.Getenv("NOLOG")
|
||||
|
||||
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@ -13044,6 +13044,14 @@
|
||||
"resolved": "https://registry.npmjs.org/ra-language-chinese/-/ra-language-chinese-2.0.5.tgz",
|
||||
"integrity": "sha512-BwaqQWDNhQX/Ufe5Ki2GrJ3k5OGmH8dKrQn/npvRik80+tpN4Ew4vbyS8o4E74B4UfSJ8Sj10YdB0bA6FZnAOA=="
|
||||
},
|
||||
"ra-language-dutch": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ra-language-dutch/-/ra-language-dutch-3.4.1.tgz",
|
||||
"integrity": "sha512-Grnmiq3ykixoykB7o+WPY+J8cBUWCBEEdHWxgZ++LeMxNiv170Xf0LEBgwnlD8+YupZUyZOL4RwEJIeoN3oVYg==",
|
||||
"requires": {
|
||||
"ra-core": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"ra-language-english": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ra-language-english/-/ra-language-english-3.4.1.tgz",
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"prop-types": "^15.7.2",
|
||||
"ra-data-json-server": "^3.4.1",
|
||||
"ra-language-chinese": "^2.0.5",
|
||||
"ra-language-dutch": "^3.4.1",
|
||||
"ra-language-french": "^3.4.1",
|
||||
"ra-language-italian": "^3.0.0",
|
||||
"ra-language-portuguese": "^1.6.0",
|
||||
@ -31,7 +32,8 @@
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"prettier": "prettier --write src/**/*.js"
|
||||
"prettier": "prettier --write src/*.js src/**/*.js",
|
||||
"check-formatting": "prettier -c src/*.js src/**/*.js"
|
||||
},
|
||||
"homepage": ".",
|
||||
"proxy": "http://localhost:4633/",
|
||||
|
||||
@ -35,8 +35,8 @@ const App = () => (
|
||||
customReducers: {
|
||||
queue: playQueueReducer,
|
||||
albumView: albumViewReducer,
|
||||
theme: themeReducer
|
||||
}
|
||||
theme: themeReducer,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Admin
|
||||
@ -70,7 +70,7 @@ const App = () => (
|
||||
) : (
|
||||
<Resource name="transcoding" />
|
||||
),
|
||||
<Player />
|
||||
<Player />,
|
||||
]}
|
||||
</Admin>
|
||||
</Provider>
|
||||
|
||||
@ -8,7 +8,7 @@ import PlayArrowIcon from '@material-ui/icons/PlayArrow'
|
||||
import ShuffleIcon from '@material-ui/icons/Shuffle'
|
||||
import React from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { playAlbum } from '../audioplayer'
|
||||
import { playAlbum, shuffleAlbum } from '../audioplayer'
|
||||
|
||||
export const AlbumActions = ({
|
||||
className,
|
||||
@ -28,17 +28,6 @@ export const AlbumActions = ({
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const shuffle = (data) => {
|
||||
const ids = Object.keys(data)
|
||||
for (let i = ids.length - 1; i > 0; i--) {
|
||||
let j = Math.floor(Math.random() * (i + 1))
|
||||
;[ids[i], ids[j]] = [ids[j], ids[i]]
|
||||
}
|
||||
const shuffled = {}
|
||||
ids.forEach((id) => (shuffled[id] = data[id]))
|
||||
return shuffled
|
||||
}
|
||||
|
||||
return (
|
||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||
<Button
|
||||
@ -51,9 +40,7 @@ export const AlbumActions = ({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const shuffled = shuffle(filteredData)
|
||||
const firstId = Object.keys(shuffled)[0]
|
||||
dispatch(playAlbum(firstId, shuffled))
|
||||
dispatch(shuffleAlbum(filteredData))
|
||||
}}
|
||||
label={translate('resources.album.actions.shuffle')}
|
||||
>
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import Player from './Player'
|
||||
import { addTrack, setTrack, playQueueReducer, playAlbum } from './queue'
|
||||
import {
|
||||
addTrack,
|
||||
setTrack,
|
||||
playQueueReducer,
|
||||
playAlbum,
|
||||
shuffleAlbum,
|
||||
} from './queue'
|
||||
|
||||
export { Player, addTrack, setTrack, playAlbum, playQueueReducer }
|
||||
export { Player, addTrack, setTrack, playAlbum, playQueueReducer, shuffleAlbum }
|
||||
|
||||
@ -26,6 +26,27 @@ const setTrack = (data) => ({
|
||||
data,
|
||||
})
|
||||
|
||||
const shuffle = (data) => {
|
||||
const ids = Object.keys(data)
|
||||
for (let i = ids.length - 1; i > 0; i--) {
|
||||
let j = Math.floor(Math.random() * (i + 1))
|
||||
;[ids[i], ids[j]] = [ids[j], ids[i]]
|
||||
}
|
||||
const shuffled = {}
|
||||
ids.forEach((id) => (shuffled[id] = data[id]))
|
||||
return shuffled
|
||||
}
|
||||
|
||||
const shuffleAlbum = (data) => {
|
||||
const shuffled = shuffle(data)
|
||||
const firstId = Object.keys(shuffled)[0]
|
||||
return {
|
||||
type: PLAYER_PLAY_ALBUM,
|
||||
id: firstId,
|
||||
data: shuffled,
|
||||
}
|
||||
}
|
||||
|
||||
const playAlbum = (id, data) => ({
|
||||
type: PLAYER_PLAY_ALBUM,
|
||||
id,
|
||||
@ -109,4 +130,12 @@ const playQueueReducer = (
|
||||
}
|
||||
}
|
||||
|
||||
export { addTrack, setTrack, playAlbum, syncQueue, scrobble, playQueueReducer }
|
||||
export {
|
||||
addTrack,
|
||||
setTrack,
|
||||
playAlbum,
|
||||
syncQueue,
|
||||
scrobble,
|
||||
shuffleAlbum,
|
||||
playQueueReducer,
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ const authProvider = {
|
||||
const request = new Request(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password }),
|
||||
headers: new Headers({ 'Content-Type': 'application/json' })
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
})
|
||||
return fetch(request)
|
||||
.then((response) => {
|
||||
@ -69,7 +69,7 @@ const authProvider = {
|
||||
getPermissions: () => {
|
||||
const role = localStorage.getItem('role')
|
||||
return role ? Promise.resolve(role) : Promise.reject()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const removeItems = () => {
|
||||
|
||||
@ -2,7 +2,7 @@ const defaultConfig = {
|
||||
version: 'dev',
|
||||
firstTime: false,
|
||||
baseURL: '',
|
||||
loginBackgroundURL: 'https://source.unsplash.com/random/1600x900?music'
|
||||
loginBackgroundURL: 'https://source.unsplash.com/random/1600x900?music',
|
||||
}
|
||||
|
||||
let config
|
||||
@ -12,7 +12,7 @@ try {
|
||||
|
||||
config = {
|
||||
...defaultConfig,
|
||||
...appConfig
|
||||
...appConfig,
|
||||
}
|
||||
} catch (e) {
|
||||
config = defaultConfig
|
||||
|
||||
@ -53,7 +53,7 @@ export default deepmerge(frenchMessages, {
|
||||
user: {
|
||||
name: 'Utilisateur |||| Utilisateurs',
|
||||
fields: {
|
||||
userName: 'Nom d\'utilisateur',
|
||||
userName: "Nom d'utilisateur",
|
||||
isAdmin: 'Administrateur',
|
||||
lastLoginAt: 'Dernière connexion',
|
||||
updatedAt: 'Dernière mise à jour',
|
||||
@ -67,7 +67,7 @@ export default deepmerge(frenchMessages, {
|
||||
transcodingId: 'Transcodage',
|
||||
maxBitRate: 'Bitrate maximum',
|
||||
client: 'Client',
|
||||
userName: 'Nom d\'utilisateur',
|
||||
userName: "Nom d'utilisateur",
|
||||
lastSeen: 'Vu pour la dernière fois',
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import en from './en'
|
||||
import cn from './cn'
|
||||
import fr from './fr'
|
||||
import it from './it'
|
||||
import nl from './nl'
|
||||
import pt from './pt'
|
||||
import cn from './cn'
|
||||
|
||||
const addLanguages = (lang) => {
|
||||
Object.keys(lang).forEach((l) => (languages[l] = deepmerge(en, lang[l])))
|
||||
@ -11,7 +12,7 @@ const addLanguages = (lang) => {
|
||||
const languages = { en }
|
||||
|
||||
// Add new languages to the object bellow (please keep alphabetic sort)
|
||||
addLanguages({ cn, fr, it, pt })
|
||||
addLanguages({ cn, fr, it, nl, pt })
|
||||
|
||||
// "Hack" to make "albumSongs" resource use the same translations as "song"
|
||||
Object.keys(languages).forEach(
|
||||
|
||||
86
ui/src/i18n/nl.js
Normal file
86
ui/src/i18n/nl.js
Normal file
@ -0,0 +1,86 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import englishMessages from 'ra-language-dutch'
|
||||
|
||||
export default deepmerge(englishMessages, {
|
||||
languageName: 'Nederlands',
|
||||
resources: {
|
||||
song: {
|
||||
name: 'Nummer |||| Nummers',
|
||||
fields: {
|
||||
albumArtist: 'Album Artiest',
|
||||
duration: 'Tijd',
|
||||
trackNumber: 'Nummer #',
|
||||
playCount: 'Aantal keren afgespeeld',
|
||||
},
|
||||
bulk: {
|
||||
addToQueue: 'Toevoegen aan afspeellijst',
|
||||
},
|
||||
},
|
||||
album: {
|
||||
fields: {
|
||||
albumArtist: 'Album Artiest',
|
||||
artist: 'Artiest',
|
||||
duration: 'Tijd',
|
||||
songCount: 'Nummerss',
|
||||
playCount: 'Aantal keren afgespeeld',
|
||||
},
|
||||
actions: {
|
||||
playAll: 'Afspelen',
|
||||
playNext: 'Hierna afspelen',
|
||||
addToQueue: 'Toevoegen aan afspeellijst',
|
||||
shuffle: 'Shuffle',
|
||||
},
|
||||
},
|
||||
},
|
||||
ra: {
|
||||
auth: {
|
||||
welcome1: 'Bedankt voor het installeren van Navidrome!',
|
||||
welcome2: 'Maak om te beginnen een beheerdersaccount',
|
||||
confirmPassword: 'Bevestig wachtwoord',
|
||||
buttonCreateAdmin: 'Beheerder maken',
|
||||
},
|
||||
validation: {
|
||||
invalidChars: 'Gebruik alleen letters en cijfers',
|
||||
passwordDoesNotMatch: 'Wachtwoord komt niet overeen',
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
library: 'Bibliotheek',
|
||||
settings: 'Instellingen',
|
||||
version: 'Versie %{version}',
|
||||
theme: 'Thema',
|
||||
personal: {
|
||||
name: 'Persoonlijk',
|
||||
options: {
|
||||
theme: 'Thema',
|
||||
language: 'Taal',
|
||||
},
|
||||
},
|
||||
},
|
||||
player: {
|
||||
playListsText: 'Afspeellijst afspelen',
|
||||
openText: 'Openen',
|
||||
closeText: 'Sluiten',
|
||||
notContentText: 'Geen muziek',
|
||||
clickToPlayText: 'Klik om af te spelen',
|
||||
clickToPauseText: 'Klik om te pauzeren',
|
||||
nextTrackText: 'Volgende',
|
||||
previousTrackText: 'Vorige',
|
||||
reloadText: 'Herladen',
|
||||
volumeText: 'Volume',
|
||||
toggleLyricText: 'Songtekst aan/uit',
|
||||
toggleMiniModeText: 'Minimaliseren',
|
||||
destroyText: 'Vernietigen',
|
||||
downloadText: 'Downloaden',
|
||||
removeAudioListsText: 'Audiolijsten verwijderen',
|
||||
controllerTitle: '',
|
||||
clickToDeleteText: `Klik om %{name} te verwijderen`,
|
||||
emptyLyricText: 'Geen songtekst',
|
||||
playModeText: {
|
||||
order: 'In volgorde',
|
||||
orderLoop: 'Herhalen',
|
||||
singleLoop: 'Herhaal Eenmalig',
|
||||
shufflePlay: 'Shuffle',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -101,7 +101,7 @@ function registerValidSW(swUrl, config) {
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' }
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
|
||||
@ -68,5 +68,5 @@ func ParamBool(r *http.Request, param string, def bool) bool {
|
||||
if p == "" {
|
||||
return def
|
||||
}
|
||||
return strings.Index("/true/on/1/", "/"+p+"/") != -1
|
||||
return strings.Contains("/true/on/1/", "/"+p+"/")
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user