feat(scrobble): add configuration option to enable scrobble history

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan
2025-12-05 21:06:29 -05:00
parent 9d4af721ca
commit 866ff8468c
8 changed files with 24 additions and 18 deletions

View File

@@ -102,7 +102,8 @@ type configOptions struct {
Spotify spotifyOptions `json:",omitzero"`
Deezer deezerOptions `json:",omitzero"`
ListenBrainz listenBrainzOptions `json:",omitzero"`
Tags map[string]TagConf `json:",omitempty"`
EnableScrobbleHistory bool
Tags map[string]TagConf `json:",omitempty"`
Agents string
// DevFlags. These are used to enable/disable debugging and incomplete features
@@ -598,6 +599,7 @@ func setViperDefaults() {
viper.SetDefault("deezer.language", "en")
viper.SetDefault("listenbrainz.enabled", true)
viper.SetDefault("listenbrainz.baseurl", "https://api.listenbrainz.org/1/")
viper.SetDefault("enablescrobblehistory", true)
viper.SetDefault("httpsecurityheaders.customframeoptionsvalue", "DENY")
viper.SetDefault("backup.path", "")
viper.SetDefault("backup.schedule", "")

View File

@@ -2,7 +2,6 @@ package scrobbler
import (
"context"
"fmt"
"maps"
"sort"
"sync"
@@ -335,10 +334,6 @@ func (p *playTracker) Submit(ctx context.Context, submissions []Submission) erro
}
func (p *playTracker) incPlay(ctx context.Context, track *model.MediaFile, timestamp time.Time) error {
user, ok := request.UserFrom(ctx)
if !ok {
return fmt.Errorf("user not found in context")
}
return p.ds.WithTx(func(tx model.DataStore) error {
err := tx.MediaFile(ctx).IncPlayCount(track.ID, timestamp)
if err != nil {
@@ -354,7 +349,10 @@ func (p *playTracker) incPlay(ctx context.Context, track *model.MediaFile, times
return err
}
}
return tx.Scrobble(ctx).RecordScrobble(track.ID, user.ID, timestamp)
if conf.Server.EnableScrobbleHistory {
return tx.Scrobble(ctx).RecordScrobble(track.ID, timestamp)
}
return nil
})
}

View File

@@ -61,7 +61,7 @@ var _ = Describe("PlayTracker", func() {
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
ctx = context.Background()
ctx = GinkgoT().Context()
ctx = request.WithUser(ctx, model.User{ID: "u-1"})
ctx = request.WithPlayer(ctx, model.Player{ScrobbleEnabled: true})
ds = &tests.MockDataStore{}
@@ -177,9 +177,9 @@ var _ = Describe("PlayTracker", func() {
track2 := track
track2.ID = "456"
_ = ds.MediaFile(ctx).Put(&track2)
ctx = request.WithUser(context.Background(), model.User{UserName: "user-1"})
ctx = request.WithUser(GinkgoT().Context(), model.User{UserName: "user-1"})
_ = tracker.NowPlaying(ctx, "player-1", "player-one", "123", 0)
ctx = request.WithUser(context.Background(), model.User{UserName: "user-2"})
ctx = request.WithUser(GinkgoT().Context(), model.User{UserName: "user-2"})
_ = tracker.NowPlaying(ctx, "player-2", "player-two", "456", 0)
playing, err := tracker.GetNowPlaying(ctx)
@@ -235,6 +235,7 @@ var _ = Describe("PlayTracker", func() {
})
It("records scrobble in repository", func() {
conf.Server.EnableScrobbleHistory = true
ctx = request.WithUser(ctx, model.User{ID: "u-1", UserName: "user-1"})
ts := time.Now()
@@ -368,7 +369,7 @@ var _ = Describe("PlayTracker", func() {
var mockedBS *mockBufferedScrobbler
BeforeEach(func() {
ctx = context.Background()
ctx = GinkgoT().Context()
ctx = request.WithUser(ctx, model.User{ID: "u-1"})
ctx = request.WithPlayer(ctx, model.Player{ScrobbleEnabled: true})
ds = &tests.MockDataStore{}

View File

@@ -9,5 +9,5 @@ type Scrobble struct {
}
type ScrobbleRepository interface {
RecordScrobble(mediaFileID, userID string, submissionTime time.Time) error
RecordScrobble(mediaFileID string, submissionTime time.Time) error
}

View File

@@ -21,7 +21,8 @@ func NewScrobbleRepository(ctx context.Context, db dbx.Builder) model.ScrobbleRe
return r
}
func (r *scrobbleRepository) RecordScrobble(mediaFileID, userID string, submissionTime time.Time) error {
func (r *scrobbleRepository) RecordScrobble(mediaFileID string, submissionTime time.Time) error {
userID := loggedUser(r.ctx).ID
values := map[string]interface{}{
"media_file_id": mediaFileID,
"user_id": userID,

View File

@@ -23,7 +23,7 @@ var _ = Describe("ScrobbleRepository", func() {
BeforeEach(func() {
fileID = id.NewRandom()
userID = id.NewRandom()
ctx = request.WithUser(log.NewContext(context.TODO()), model.User{ID: "userid", UserName: "johndoe", IsAdmin: true})
ctx = request.WithUser(log.NewContext(GinkgoT().Context()), model.User{ID: userID, UserName: "johndoe", IsAdmin: true})
db := GetDBXBuilder()
repo = NewScrobbleRepository(ctx, db)
@@ -63,7 +63,7 @@ var _ = Describe("ScrobbleRepository", func() {
}).Execute()
Expect(err).ToNot(HaveOccurred())
err = repo.RecordScrobble(fileID, userID, submissionTime)
err = repo.RecordScrobble(fileID, submissionTime)
Expect(err).ToNot(HaveOccurred())
// Verify insertion

View File

@@ -220,7 +220,7 @@ func (db *MockDataStore) Scrobble(ctx context.Context) model.ScrobbleRepository
if db.RealDS != nil {
db.MockedScrobble = db.RealDS.Scrobble(ctx)
} else {
db.MockedScrobble = &MockScrobbleRepo{}
db.MockedScrobble = &MockScrobbleRepo{ctx: ctx}
}
}
return db.MockedScrobble

View File

@@ -1,19 +1,23 @@
package tests
import (
"context"
"time"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
)
type MockScrobbleRepo struct {
RecordedScrobbles []model.Scrobble
ctx context.Context
}
func (m *MockScrobbleRepo) RecordScrobble(fileID, userID string, submissionTime time.Time) error {
func (m *MockScrobbleRepo) RecordScrobble(fileID string, submissionTime time.Time) error {
user, _ := request.UserFrom(m.ctx)
m.RecordedScrobbles = append(m.RecordedScrobbles, model.Scrobble{
MediaFileID: fileID,
UserID: userID,
UserID: user.ID,
SubmissionTime: submissionTime,
})
return nil