Merge branch 'master' of https://github.com/deluan/navidrome into gridplay-feature

This commit is contained in:
Srihari Chandana 2020-04-26 21:55:13 -04:00
commit 636c4f3740
56 changed files with 285 additions and 137 deletions

View File

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

@ -0,0 +1,11 @@
linters:
enable:
- goimports
- unconvert
- gosec
issues:
exclude-rules:
- linters:
- gosec
text: "(G501|G401):"

View File

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

View File

@ -2,6 +2,7 @@ package migration
import (
"database/sql"
"github.com/pressly/goose"
)

View File

@ -2,6 +2,7 @@ package migration
import (
"database/sql"
"github.com/pressly/goose"
)

View File

@ -2,6 +2,7 @@ package migration
import (
"database/sql"
"github.com/pressly/goose"
)

View File

@ -2,6 +2,7 @@ package migration
import (
"database/sql"
"github.com/pressly/goose"
)

View File

@ -2,6 +2,7 @@ package migration
import (
"database/sql"
"github.com/pressly/goose"
)

View File

@ -2,6 +2,7 @@ package migration
import (
"database/sql"
"github.com/pressly/goose"
)

View File

@ -2,6 +2,7 @@ package migration
import (
"database/sql"
"github.com/pressly/goose"
)

View File

@ -2,6 +2,7 @@ package migration
import (
"database/sql"
"github.com/pressly/goose"
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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+"/")
}