Compare commits

...

20 Commits

Author SHA1 Message Date
Deluan
28bc9c1d4f fix: AlbumShow was adding previous played tracks when trying to shuffle the album 2020-03-02 14:51:52 -05:00
Deluan
5e7aaa667b fix: missing id in queue items was preventing scrobble to work properly 2020-03-02 14:20:57 -05:00
Deluan
1afc495920 chore: upgrade react, react-dom and react-redux 2020-03-02 13:06:27 -05:00
Deluan
cf7d877714 chore: upgrade @testing-library/user-event 2020-03-02 12:04:58 -05:00
Deluan
81831da67a chore: upgrade react-admin 2020-03-02 11:58:23 -05:00
Deluan
fcd2fcae67 chore: upgrade @testing-library, react-scripts 2020-03-02 11:52:06 -05:00
Deluan
1c33b0aea8 docs: update API compatibility chart 2020-03-02 09:48:46 -05:00
Deluan
fc06163b5a refactor: remove superfluous (and untested) code 2020-03-02 09:37:47 -05:00
Deluan
72f0a6fb66 chore: removed unused (video) mime types 2020-03-02 00:16:15 -05:00
Deluan
6f5a322927 fix: login must be case-insensitive 2020-03-01 15:45:41 -05:00
Deluan
a7f8e4ee2b fix: only set created_at when adding data to DB 2020-02-28 18:43:22 -05:00
Deluan
0850872b0f fix: ormer.Driver() is not available when creating orms with NewOrmWithDB() 2020-02-28 16:09:27 -05:00
Deluan
1d886156d5 feat: better SQLite3 configuration, to avoid DB contention 2020-02-28 15:06:31 -05:00
Deluan
faa2a978c0 refactor: use only one DB instance for the whole application 2020-02-28 15:06:31 -05:00
Deluan
38faffa907 feat: notice function to notify (in logs) about important changes in migrations 2020-02-28 14:00:41 -05:00
Deluan
65a792be3a fix: handle nil pointer dereference 2020-02-28 11:02:38 -05:00
Deluan
876354e58e feat: MaxTranscodingCacheSize is now specified in MB 2020-02-26 14:08:14 -05:00
Deluan
14b33bc34d fix: there are no docker images available for node 13.9 2020-02-26 12:00:00 -05:00
Deluan
9044aa8740 chore: upgrade NodeJS to 13.9.0 2020-02-26 09:52:25 -05:00
Deluan
07ac14f810 chore: upgrade Go to 1.14 2020-02-26 09:37:48 -05:00
30 changed files with 1018 additions and 783 deletions

View File

@@ -11,10 +11,10 @@ jobs:
os: [macOS-latest, ubuntu-latest]
steps:
- name: Set up Go 1.13
- name: Set up Go 1.14
uses: actions/setup-go@v1
with:
go-version: 1.13
go-version: 1.14
id: go
- name: Check out code into the Go module directory

View File

@@ -24,7 +24,7 @@ jobs:
- name: Fetch tags
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- name: Run GoReleaser
uses: docker://bepsays/ci-goreleaser:latest
uses: docker://bepsays/ci-goreleaser:1.14-1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.nvmrc
View File

@@ -1 +1 @@
v13.7.0
v13.9.0

View File

@@ -10,7 +10,7 @@ Navidrome and Subsonic:
* Right now, Navidrome only works with a single Music Library (Music Folder)
* Navidrome does not mark songs as played by calls to `stream`, only when
`scrobble` is called with `submission=true`
* Next features to be implemented: Playlists (WIP), MultiUser (WIP), Jukebox, Sharing, Podcasts, Bookmarks, Internet Radio.
* Next features to be implemented: Last.FM integration, Jukebox, Sharing, Bookmarks, Podcasts, Internet Radio.
Navidrome is actively being tested with:
[DSub](http://www.subsonic.org/pages/apps.jsp#dsub),
@@ -54,7 +54,7 @@ Navidrome is actively being tested with:
| `deletePlaylist` | |
| ||
| _MEDIA RETRIEVAL_ ||
| `stream` | No Transcoding/Downsampling support (for now)|
| `stream` | Experimental Transcoding/Downsampling support available |
| `download` | |
| `getCoverArt` | Only gets embedded artwork |
| `getAvatar` | Always returns the same image |
@@ -62,7 +62,7 @@ Navidrome is actively being tested with:
| _MEDIA ANNOTATION_ ||
| `star` | |
| `unstar` | |
| `setRating` | Doesn't work with artists |
| `setRating` | |
| `scrobble` | No Last.FM support yet. It is used to update play count and last played |
| ||
| _USER MANAGEMENT_ ||

View File

@@ -1,6 +1,6 @@
#####################################################
### Build UI bundles
FROM node:13.7-alpine AS jsbuilder
FROM node:13.8-alpine AS jsbuilder
WORKDIR /src
COPY ui/package.json ui/package-lock.json ./
RUN npm ci
@@ -10,7 +10,7 @@ RUN npm run build
#####################################################
### Build executable
FROM golang:1.13-alpine AS gobuilder
FROM golang:1.14-alpine AS gobuilder
# Download build tools
RUN mkdir -p /src/ui/build

View File

@@ -1,5 +1,5 @@
GO_VERSION=1.13
NODE_VERSION=v13.7.0
GO_VERSION=1.14
NODE_VERSION=v13.9.0
GIT_SHA=$(shell git rev-parse --short HEAD)
@@ -51,7 +51,7 @@ check_env: check_go_env check_node_env
.PHONY: check_go_env
check_go_env:
@(hash go) || (echo "\nERROR: GO environment not setup properly!\n"; exit 1)
@go version | grep -q $(GO_VERSION) || (echo "\nERROR: Please upgrade your GO version\n"; exit 1)
@go version | grep -q $(GO_VERSION) || (echo "\nERROR: Please upgrade your GO version\nThis project requires version $(GO_VERSION)"; exit 1)
.PHONY: check_node_env
check_node_env:
@@ -79,4 +79,4 @@ release:
.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
docker run -it -v $(PWD):/github/workspace -w /github/workspace bepsays/ci-goreleaser:1.14-1 goreleaser release --rm-dist --skip-publish --snapshot

View File

@@ -86,7 +86,7 @@ To get the cutting-edge, latest version from master, use the image `deluan/navid
### Build from source
You will need to install [Go 1.13](https://golang.org/dl/) and [Node 13.7.0](http://nodejs.org).
You will need to install [Go 1.14](https://golang.org/dl/) and [Node 13.9.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)

View File

@@ -25,7 +25,7 @@ type nd struct {
EnableDownsampling bool `default:"false"`
MaxBitRate int `default:"0"`
MaxTranscodingCacheSize int64 `default:"100000000"` // 100MB
MaxTranscodingCacheSize int64 `default:"100"` // in MB
DownsampleCommand string `default:"ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -"`
ProbeCommand string `default:"ffmpeg -i %s -f ffmetadata"`
@@ -83,7 +83,7 @@ func LoadFromFile(confFile string, skipFlags ...bool) {
os.Exit(2)
}
if Server.DbPath == "" {
Server.DbPath = filepath.Join(Server.DataFolder, "navidrome.db")
Server.DbPath = filepath.Join(Server.DataFolder, consts.DefaultDbPath)
}
if os.Getenv("PORT") != "" {
Server.Port = os.Getenv("PORT")

View File

@@ -6,6 +6,7 @@ const (
AppName = "navidrome"
LocalConfigFile = "./navidrome.toml"
DefaultDbPath = "navidrome.db?cache=shared&_busy_timeout=15000&_journal_mode=WAL"
InitialSetupFlagKey = "InitialSetup"
JWTSecretKey = "JWTSecret"

View File

@@ -8,7 +8,6 @@ func init() {
".ogg": "audio/ogg",
".oga": "audio/ogg",
".opus": "audio/ogg",
".ogx": "application/ogg",
".aac": "audio/mp4",
".m4a": "audio/mp4",
".m4b": "audio/mp4",
@@ -18,20 +17,8 @@ func init() {
".ape": "audio/x-monkeys-audio",
".mpc": "audio/x-musepack",
".shn": "audio/x-shn",
".flv": "video/x-flv",
".avi": "video/avi",
".mpg": "video/mpeg",
".mpeg": "video/mpeg",
".mp4": "video/mp4",
".m4v": "video/x-m4v",
".mkv": "video/x-matroska",
".mov": "video/quicktime",
".wmv": "video/x-ms-wmv",
".ogv": "video/ogg",
".divx": "video/divx",
".m2ts": "video/MP2T",
".ts": "video/MP2T",
".webm": "video/webm",
".aif": "audio/x-aiff",
".aiff": "audio/x-aiff",
".gif": "image/gif",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",

View File

@@ -13,32 +13,36 @@ import (
)
var (
once sync.Once
Driver = "sqlite3"
Path string
)
func Init() {
var (
once sync.Once
db *sql.DB
)
func Db() *sql.DB {
once.Do(func() {
var err error
Path = conf.Server.DbPath
if Path == ":memory:" {
Path = "file::memory:?cache=shared"
conf.Server.DbPath = Path
}
log.Debug("Opening DataBase", "dbPath", Path, "driver", Driver)
db, err = sql.Open(Driver, Path)
if err != nil {
panic(err)
}
})
return db
}
func EnsureLatestVersion() {
Init()
db, err := sql.Open(Driver, Path)
defer db.Close()
if err != nil {
log.Error("Failed to open DB", err)
os.Exit(1)
}
db := Db()
err = goose.SetDialect(Driver)
err := goose.SetDialect(Driver)
if err != nil {
log.Error("Invalid DB driver", "driver", Driver, err)
os.Exit(1)

View File

@@ -2,7 +2,7 @@ package migration
import (
"database/sql"
"github.com/deluan/navidrome/log"
"github.com/pressly/goose"
)
@@ -11,7 +11,7 @@ func init() {
}
func Up20200220143731(tx *sql.Tx) error {
log.Warn("This migration will force the next scan to be a full rescan!")
notice(tx, "This migration will force the next scan to be a full rescan!")
_, err := tx.Exec(`
create table media_file_dg_tmp
(

46
db/migration/migration.go Normal file
View File

@@ -0,0 +1,46 @@
package migration
import (
"database/sql"
"fmt"
"sync"
"github.com/deluan/navidrome/consts"
)
// Use this in migrations that need to communicate something important (braking changes, forced reindexes, etc...)
func notice(tx *sql.Tx, msg string) {
if isDBInitialized(tx) {
fmt.Printf(`
*************************************************************************************
NOTICE: %s
*************************************************************************************
`, msg)
}
}
var once sync.Once
func isDBInitialized(tx *sql.Tx) (initialized bool) {
once.Do(func() {
rows, err := tx.Query("select count(*) from property where id='" + consts.InitialSetupFlagKey + "'")
checkErr(err)
initialized = checkCount(rows) > 0
})
return initialized
}
func checkCount(rows *sql.Rows) (count int) {
for rows.Next() {
err := rows.Scan(&count)
checkErr(err)
}
return count
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}

View File

@@ -172,7 +172,7 @@ func getFinalCachedSize(r fscache.ReadAtCloser) int64 {
}
func NewTranscodingCache() (fscache.Cache, error) {
lru := fscache.NewLRUHaunter(0, conf.Server.MaxTranscodingCacheSize, 10*time.Minute)
lru := fscache.NewLRUHaunter(0, conf.Server.MaxTranscodingCacheSize*1024*1024, 10*time.Minute)
h := fscache.NewLRUHaunterStrategy(lru)
cacheFolder := filepath.Join(conf.Server.DataFolder, consts.CacheDir)
fs, err := fscache.NewFs(cacheFolder, 0755)

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/deluan/navidrome
go 1.13
go 1.14
require (
github.com/BurntSushi/toml v0.3.1 // indirect

View File

@@ -22,6 +22,7 @@ type UserRepository interface {
CountAll(...QueryOptions) (int64, error)
Get(id string) (*User, error)
Put(*User) error
// FindByUsername must be case-insensitive
FindByUsername(username string) (*User, error)
UpdateLastLoginAt(id string) error
UpdateLastAccessAt(id string) error

View File

@@ -71,12 +71,7 @@ func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, e
// TODO Keep order when paginating
func (r *albumRepository) GetRandom(options ...model.QueryOptions) (model.Albums, error) {
sq := r.selectAlbum(options...)
switch r.ormer.Driver().Type() {
case orm.DRMySQL:
sq = sq.OrderBy("RAND()")
default:
sq = sq.OrderBy("RANDOM()")
}
sq = sq.OrderBy("RANDOM()")
results := model.Albums{}
err := r.queryAll(sq, &results)
return results, err

View File

@@ -100,12 +100,7 @@ func (r mediaFileRepository) GetStarred(options ...model.QueryOptions) (model.Me
// TODO Keep order when paginating
func (r mediaFileRepository) GetRandom(options ...model.QueryOptions) (model.MediaFiles, error) {
sq := r.selectMediaFile(options...)
switch r.ormer.Driver().Type() {
case orm.DRMySQL:
sq = sq.OrderBy("RAND()")
default:
sq = sq.OrderBy("RANDOM()")
}
sq = sq.OrderBy("RANDOM()")
results := model.MediaFiles{}
err := r.queryAll(sq, &results)
return results, err

View File

@@ -20,65 +20,62 @@ type SQLStore struct {
}
func New() model.DataStore {
once.Do(func() {
err := orm.RegisterDataBase("default", db.Driver, db.Path)
if err != nil {
panic(err)
}
})
return &SQLStore{}
}
func (db *SQLStore) Album(ctx context.Context) model.AlbumRepository {
return NewAlbumRepository(ctx, db.getOrmer())
func (s *SQLStore) Album(ctx context.Context) model.AlbumRepository {
return NewAlbumRepository(ctx, s.getOrmer())
}
func (db *SQLStore) Artist(ctx context.Context) model.ArtistRepository {
return NewArtistRepository(ctx, db.getOrmer())
func (s *SQLStore) Artist(ctx context.Context) model.ArtistRepository {
return NewArtistRepository(ctx, s.getOrmer())
}
func (db *SQLStore) MediaFile(ctx context.Context) model.MediaFileRepository {
return NewMediaFileRepository(ctx, db.getOrmer())
func (s *SQLStore) MediaFile(ctx context.Context) model.MediaFileRepository {
return NewMediaFileRepository(ctx, s.getOrmer())
}
func (db *SQLStore) MediaFolder(ctx context.Context) model.MediaFolderRepository {
return NewMediaFolderRepository(ctx, db.getOrmer())
func (s *SQLStore) MediaFolder(ctx context.Context) model.MediaFolderRepository {
return NewMediaFolderRepository(ctx, s.getOrmer())
}
func (db *SQLStore) Genre(ctx context.Context) model.GenreRepository {
return NewGenreRepository(ctx, db.getOrmer())
func (s *SQLStore) Genre(ctx context.Context) model.GenreRepository {
return NewGenreRepository(ctx, s.getOrmer())
}
func (db *SQLStore) Playlist(ctx context.Context) model.PlaylistRepository {
return NewPlaylistRepository(ctx, db.getOrmer())
func (s *SQLStore) Playlist(ctx context.Context) model.PlaylistRepository {
return NewPlaylistRepository(ctx, s.getOrmer())
}
func (db *SQLStore) Property(ctx context.Context) model.PropertyRepository {
return NewPropertyRepository(ctx, db.getOrmer())
func (s *SQLStore) Property(ctx context.Context) model.PropertyRepository {
return NewPropertyRepository(ctx, s.getOrmer())
}
func (db *SQLStore) User(ctx context.Context) model.UserRepository {
return NewUserRepository(ctx, db.getOrmer())
func (s *SQLStore) User(ctx context.Context) model.UserRepository {
return NewUserRepository(ctx, s.getOrmer())
}
func (db *SQLStore) Resource(ctx context.Context, m interface{}) model.ResourceRepository {
func (s *SQLStore) Resource(ctx context.Context, m interface{}) model.ResourceRepository {
switch m.(type) {
case model.User:
return db.User(ctx).(model.ResourceRepository)
return s.User(ctx).(model.ResourceRepository)
case model.Artist:
return db.Artist(ctx).(model.ResourceRepository)
return s.Artist(ctx).(model.ResourceRepository)
case model.Album:
return db.Album(ctx).(model.ResourceRepository)
return s.Album(ctx).(model.ResourceRepository)
case model.MediaFile:
return db.MediaFile(ctx).(model.ResourceRepository)
return s.MediaFile(ctx).(model.ResourceRepository)
}
log.Error("Resource no implemented", "model", reflect.TypeOf(m).Name())
return nil
}
func (db *SQLStore) WithTx(block func(tx model.DataStore) error) error {
o := orm.NewOrm()
err := o.Begin()
func (s *SQLStore) WithTx(block func(tx model.DataStore) error) error {
o, err := orm.NewOrmWithDB(db.Driver, "default", db.Db())
if err != nil {
return err
}
err = o.Begin()
if err != nil {
return err
}
@@ -101,41 +98,45 @@ func (db *SQLStore) WithTx(block func(tx model.DataStore) error) error {
return nil
}
func (db *SQLStore) GC(ctx context.Context) error {
err := db.Album(ctx).PurgeEmpty()
func (s *SQLStore) GC(ctx context.Context) error {
err := s.Album(ctx).PurgeEmpty()
if err != nil {
return err
}
err = db.Artist(ctx).PurgeEmpty()
err = s.Artist(ctx).PurgeEmpty()
if err != nil {
return err
}
err = db.MediaFile(ctx).(*mediaFileRepository).cleanSearchIndex()
err = s.MediaFile(ctx).(*mediaFileRepository).cleanSearchIndex()
if err != nil {
return err
}
err = db.Album(ctx).(*albumRepository).cleanSearchIndex()
err = s.Album(ctx).(*albumRepository).cleanSearchIndex()
if err != nil {
return err
}
err = db.Artist(ctx).(*artistRepository).cleanSearchIndex()
err = s.Artist(ctx).(*artistRepository).cleanSearchIndex()
if err != nil {
return err
}
err = db.MediaFile(ctx).(*mediaFileRepository).cleanAnnotations()
err = s.MediaFile(ctx).(*mediaFileRepository).cleanAnnotations()
if err != nil {
return err
}
err = db.Album(ctx).(*albumRepository).cleanAnnotations()
err = s.Album(ctx).(*albumRepository).cleanAnnotations()
if err != nil {
return err
}
return db.Artist(ctx).(*artistRepository).cleanAnnotations()
return s.Artist(ctx).(*artistRepository).cleanAnnotations()
}
func (db *SQLStore) getOrmer() orm.Ormer {
if db.orm == nil {
return orm.NewOrm()
func (s *SQLStore) getOrmer() orm.Ormer {
if s.orm == nil {
o, err := orm.NewOrmWithDB(db.Driver, "default", db.Db())
if err != nil {
log.Error("Error obtaining new orm instance", err)
}
return o
}
return db.orm
return s.orm
}

View File

@@ -21,8 +21,8 @@ func TestPersistence(t *testing.T) {
//os.Remove("./test-123.db")
//conf.Server.Path = "./test-123.db"
conf.Server.DbPath = ":memory:"
db.Init()
conf.Server.DbPath = "file::memory:?cache=shared"
orm.RegisterDataBase("default", db.Driver, conf.Server.DbPath)
New()
db.EnsureLatestVersion()
log.SetLevel(log.LevelCritical)

View File

@@ -99,8 +99,11 @@ func (r sqlRepository) executeSQL(sq Sqlizer) (int64, error) {
return 0, err
}
start := time.Now()
var c int64
res, err := r.ormer.Raw(query, args...).Exec()
c, _ := res.RowsAffected()
if res != nil {
c, _ = res.RowsAffected()
}
r.logSQL(query, args, err, c, start)
if err != nil {
if err.Error() != "LastInsertId is not supported by this driver" {
@@ -157,6 +160,8 @@ func (r sqlRepository) count(countQuery SelectBuilder, options ...model.QueryOpt
func (r sqlRepository) put(id string, m interface{}) (newId string, err error) {
values, _ := toSqlArgs(m)
createdAt := values["created_at"]
delete(values, "created_at")
if id != "" {
update := Update(r.tableName).Where(Eq{"id": id}).SetMap(values)
count, err := r.executeSQL(update)
@@ -173,6 +178,9 @@ func (r sqlRepository) put(id string, m interface{}) (newId string, err error) {
id = rand.String()
values["id"] = id
}
if createdAt != nil {
values["created_at"] = createdAt
}
insert := Insert(r.tableName).SetMap(values)
_, err = r.executeSQL(insert)
return id, err

View File

@@ -65,6 +65,7 @@ func (r *userRepository) Put(u *model.User) error {
}
func (r *userRepository) FindByUsername(username string) (*model.User, error) {
username = strings.ToLower(username)
sel := r.newSelect().Columns("*").Where(Eq{"user_name": username})
var usr model.User
err := r.queryOne(sel, &usr)

View File

@@ -0,0 +1,41 @@
package persistence
import (
"github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("UserRepository", func() {
var repo model.UserRepository
BeforeEach(func() {
repo = NewUserRepository(log.NewContext(nil), orm.NewOrm())
})
Describe("Put/Get/FindByUsername", func() {
usr := model.User{
ID: "123",
UserName: "AdMiN",
Name: "Admin",
Email: "admin@admin.com",
Password: "wordpass",
IsAdmin: true,
}
It("saves the user to the DB", func() {
Expect(repo.Put(&usr)).To(BeNil())
})
It("returns the newly created user", func() {
actual, err := repo.Get("123")
Expect(err).ToNot(HaveOccurred())
Expect(actual.Name).To(Equal("Admin"))
})
It("find the user by case-insensitive username", func() {
actual, err := repo.FindByUsername("aDmIn")
Expect(err).ToNot(HaveOccurred())
Expect(actual.Name).To(Equal("Admin"))
})
})
})

View File

@@ -68,13 +68,12 @@ func (api *Router) routes() http.Handler {
H(r, "getIndexes", c.GetIndexes)
H(r, "getArtists", c.GetArtists)
H(r, "getGenres", c.GetGenres)
reqParams := r.With(requiredParams("id"))
H(reqParams, "getMusicDirectory", c.GetMusicDirectory)
H(reqParams, "getArtist", c.GetArtist)
H(reqParams, "getAlbum", c.GetAlbum)
H(reqParams, "getSong", c.GetSong)
H(reqParams, "getArtistInfo", c.GetArtistInfo)
H(reqParams, "getArtistInfo2", c.GetArtistInfo2)
H(r, "getMusicDirectory", c.GetMusicDirectory)
H(r, "getArtist", c.GetArtist)
H(r, "getAlbum", c.GetAlbum)
H(r, "getSong", c.GetSong)
H(r, "getArtistInfo", c.GetArtistInfo)
H(r, "getArtistInfo2", c.GetArtistInfo2)
})
r.Group(func(r chi.Router) {
c := initAlbumListController(api)

View File

@@ -89,18 +89,3 @@ func authenticate(users engine.Users) func(next http.Handler) http.Handler {
})
}
}
func requiredParams(params ...string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, p := range params {
_, err := RequiredParamString(r, p, fmt.Sprintf("%s parameter is required", p))
if err != nil {
SendError(w, r, err)
return
}
}
next.ServeHTTP(w, r)
})
}
}

View File

@@ -1,6 +1,6 @@
DevDisableAuthentication = false
User = "deluan"
Password = "wordpass"
DbPath = ":memory:"
DbPath = "file::memory:?cache=shared"
MusicFolder = "./tests/itunes-library.xml"
DownsampleCommand = "ffmpeg -i %s -b:a %bk mp3 -"

1455
ui/package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -3,20 +3,20 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.0.2",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^8.0.4",
"@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^9.4.1",
"@testing-library/user-event": "^10.0.0",
"deepmerge": "^4.2.2",
"jwt-decode": "^2.2.0",
"md5-hex": "^3.0.1",
"prop-types": "^15.7.2",
"ra-data-json-server": "^3.1.2",
"react": "^16.12.0",
"react-admin": "^3.1.2",
"react-dom": "^16.12.0",
"ra-data-json-server": "^3.2.3",
"react": "^16.13.0",
"react-admin": "^3.2.3",
"react-dom": "^16.13.0",
"react-jinke-music-player": "^4.7.2",
"react-redux": "^7.1.0",
"react-scripts": "3.3.0"
"react-redux": "^7.2.0",
"react-scripts": "^3.4.0"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -21,6 +21,13 @@ export const AlbumActions = ({
const dispatch = useDispatch()
const translate = useTranslate()
// TODO Not sure why data is accumulating tracks from previous plays... Needs investigation. For now, filter out
// the unwanted tracks
const filteredData = ids.reduce((acc, id) => {
acc[id] = data[id]
return acc
}, {})
const shuffle = (data) => {
const ids = Object.keys(data)
for (let i = ids.length - 1; i > 0; i--) {
@@ -37,7 +44,7 @@ export const AlbumActions = ({
<Button
color={'secondary'}
onClick={() => {
dispatch(playAlbum(ids[0], data))
dispatch(playAlbum(ids[0], filteredData))
}}
label={translate('resources.album.actions.playAll')}
>
@@ -46,7 +53,7 @@ export const AlbumActions = ({
<Button
color={'secondary'}
onClick={() => {
const shuffled = shuffle(data)
const shuffled = shuffle(filteredData)
const firstId = Object.keys(shuffled)[0]
dispatch(playAlbum(firstId, shuffled))
}}

View File

@@ -8,6 +8,7 @@ const PLAYER_SCROBBLE = 'PLAYER_SCROBBLE'
const PLAYER_PLAY_ALBUM = 'PLAYER_PLAY_ALBUM'
const mapToAudioLists = (item) => ({
// id: item.id,
name: item.title,
singer: item.artist,
cover: subsonicUrl('getCoverArt', item.id, { size: 300 }),