package plugins
import (
"context"
"fmt"
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/plugins/host"
)
const maxScrobbleItems = 5000
type scrobbleRetrieverServiceImpl struct {
ds model.DataStore
users userAccess
}
func newScrobbleRetrieverService(ds model.DataStore, users userAccess) host.ScrobbleRetrieverService {
return &scrobbleRetrieverServiceImpl{
ds: ds,
users: users,
}
}
func (s *scrobbleRetrieverServiceImpl) getUserContext(ctx context.Context, username string) (context.Context, error) {
usr, err := s.users.resolve(ctx, s.ds, username)
if err != nil {
return nil, fmt.Errorf("scrobbleRetriever: %w", err)
}
ctx = request.WithUser(ctx, *usr)
return ctx, nil
}
func (s *scrobbleRetrieverServiceImpl) getFirstLastScrobble(ctx context.Context, username string, order string) (*int64, error) {
ctx, err := s.getUserContext(ctx, username)
if err != nil {
return nil, err
}
scrobbles, err := s.ds.Scrobble().GetAll(ctx, model.QueryOptions{Sort: "submission_time", Order: order, Max: 1})
if err != nil {
return nil, err
}
if len(scrobbles) == 0 {
return nil, nil
}
return &scrobbles[0].SubmissionTime, nil
}
func (s *scrobbleRetrieverServiceImpl) GetFirstTimestamp(ctx context.Context, username string) (*int64, error) {
return s.getFirstLastScrobble(ctx, username, "ASC")
}
func (s *scrobbleRetrieverServiceImpl) GetLastTimestamp(ctx context.Context, username string) (*int64, error) {
return s.getFirstLastScrobble(ctx, username, "DESC")
}
func (s *scrobbleRetrieverServiceImpl) GetScrobbles(ctx context.Context, username string, options host.ScrobbleOptions) ([]host.ScrobbleRef, *host.ScrobbleOptions, error) {
ctx, err := s.getUserContext(ctx, username)
if err != nil {
return nil, nil, err
}
if options.MaxItems < 1 || options.MaxItems > maxScrobbleItems {
options.MaxItems = maxScrobbleItems
}
options.Offset = max(options.Offset, 0)
order := "ASC"
if options.Descending {
order = "DESC"
}
lookahead := options.MaxItems + 1
scrobbles, err := s.ds.Scrobble().GetAll(ctx, model.QueryOptions{
Max: lookahead,
Filters: scrobbleRangeFilters(options.FromTimestamp, options.ToTimestamp),
Sort: "scrobbles.submission_time, scrobbles.id",
Order: order,
Offset: options.Offset,
})
if err != nil {
return nil, nil, err
}
var next *host.ScrobbleOptions
targetLen := len(scrobbles)
if len(scrobbles) == lookahead {
nextTimestamp := scrobbles[lookahead-1].SubmissionTime
targetLen = lookahead - 1
ties := 0
for i := targetLen - 1; i >= 0; i-- {
if scrobbles[i].SubmissionTime != nextTimestamp {
break
}
ties++
}
if ties == targetLen {
ties += options.Offset
}
advanced := options
advanced.Offset = ties
if options.Descending {
advanced.ToTimestamp = &nextTimestamp
} else {
advanced.FromTimestamp = &nextTimestamp
}
next = &advanced
}
scrobbleRefs := make([]host.ScrobbleRef, targetLen)
for idx, scrobble := range scrobbles[:targetLen] {
scrobbleRefs[idx] = host.ScrobbleRef{
ID: scrobble.ID,
MediaFileID: scrobble.MediaFileID,
SubmissionTime: scrobble.SubmissionTime,
}
}
return scrobbleRefs, next, nil
}
func (s *scrobbleRetrieverServiceImpl) GetScrobbleCount(ctx context.Context, username string, options host.ScrobbleCountOptions) (int64, error) {
ctx, err := s.getUserContext(ctx, username)
if err != nil {
return 0, err
}
return s.ds.Scrobble().CountAll(ctx, model.QueryOptions{
Filters: scrobbleRangeFilters(options.FromTimestamp, options.ToTimestamp),
})
}
func scrobbleRangeFilters(from, to *int64) squirrel.And {
var filters squirrel.And
if from != nil {
filters = append(filters, squirrel.GtOrEq{"scrobbles.submission_time": *from})
}
if to != nil {
filters = append(filters, squirrel.LtOrEq{"scrobbles.submission_time": *to})
}
return filters
}
var _ host.ScrobbleRetrieverService = (*scrobbleRetrieverServiceImpl)(nil)