🏡 index : quine-global/quintodrome.git

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

// maxScrobbleItems caps how many scrobbles a single GetScrobbles call can return.
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"
	}

	// Fetch one more item than requested. The last item is the next timestamp to fetch
	lookahead := options.MaxItems + 1

	scrobbles, err := s.ds.Scrobble().GetAll(ctx, model.QueryOptions{
		Max:     lookahead,
		Filters: scrobbleRangeFilters(options.FromTimestamp, options.ToTimestamp),
		// The id tiebreak makes the order of equal timestamps stable, which is what
		// lets Offset skip exactly the ties already returned
		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++
		}

		// Every scrobble in this page shares the timestamp, so the ties skipped by the
		// incoming offset are still ahead of us and must be carried over
		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)