feat: add scrobble retrieval api

This commit is contained in:
Kendall Garner 2026-07-11 10:05:13 -07:00
parent 43e9ade8a3
commit 1ad8b59dbc
No known key found for this signature in database
GPG Key ID: 9355F387FE765C94
4 changed files with 39 additions and 58 deletions

View File

@ -3,12 +3,10 @@ package model
import "time"
type Scrobble struct {
MediaFileID string `json:"-"`
UserID string `json:"-"`
ID int64 `structs:"id" json:"id"`
MediaFileID string `structs:"media_file_id" json:"mediaFileId"`
UserID string `json:"-"`
SubmissionTime time.Time `structs:"submission_time" json:"submissionTime"`
RowId int64 `structs:"row_id" json:"rowId"`
MediaFile
}
type ScrobbleRepository interface {

View File

@ -16,15 +16,15 @@ type scrobbleRepository struct {
}
type dbScrobble struct {
dbMediaFile
RowId int64 `structs:"row_id" json:"rowId"`
SubmissionTime int64 `structs:"submission_time" json:"submissionTime"`
MediaFileID string `structs:"media_file_id" json:"mediaFileId"`
RowId int64 `structs:"row_id" json:"rowId"`
SubmissionTime int64 `structs:"submission_time" json:"submissionTime"`
}
func (m dbScrobble) toScrobble() model.Scrobble {
return model.Scrobble{
MediaFile: *m.MediaFile,
RowId: m.RowId,
MediaFileID: m.MediaFileID,
ID: m.RowId,
SubmissionTime: time.Unix(m.SubmissionTime, 0),
}
}
@ -49,20 +49,7 @@ func (r *scrobbleRepository) baseQuery(options ...model.QueryOptions) SelectBuil
user := loggedUser(r.ctx)
return r.newSelect(options...).
Columns("scrobbles.ROWID row_id", "submission_time", "media_file.*", "library.path as library_path", "library.name as library_name").
Join("media_file on media_file.id = media_file_id").
LeftJoin("library on media_file.library_id = library.id").
LeftJoin("annotation on ("+
"annotation.item_id = media_file.id"+
" AND annotation.item_type = 'media_file'"+
" AND annotation.user_id = '"+user.ID+"')").
Columns(
"coalesce(starred, 0) as starred",
"coalesce(rating, 0) as rating",
"starred_at",
"play_date",
"coalesce(play_count, 0) as play_count",
).
Columns("scrobbles.ROWID row_id", "media_file_id", "submission_time").
Where(Eq{"scrobbles.user_id": user.ID})
}
@ -72,9 +59,8 @@ func NewScrobbleRepository(ctx context.Context, db dbx.Builder) model.ScrobbleRe
r.db = db
r.tableName = "scrobbles"
r.registerModel(&model.Scrobble{}, map[string]filterFunc{
"from": fromTs,
"to": toTs,
"title": fullTextFilter("media_file"),
"from": fromTs,
"to": toTs,
})
r.setSortMappings(map[string]string{
"submission_time": "submission_time",
@ -95,17 +81,9 @@ func (r *scrobbleRepository) RecordScrobble(mediaFileID string, submissionTime t
}
func (r *scrobbleRepository) CountAll(options ...model.QueryOptions) (int64, error) {
user := loggedUser(r.ctx)
sel := r.newSelect().
Columns("count(*) count").
Join("media_file on media_file.id = media_file_id").
Where(Eq{"user_id": user.ID})
sel = r.applyFilters(sel, options...)
count := r.baseQuery(options...).RemoveColumns().Column("COUNT() as count").RemoveOffset().RemoveLimit().OrderBy("scrobbles.ROWID")
var res struct{ Count int64 }
err := r.queryOne(sel, &res)
err := r.queryOne(count, &res)
return res.Count, err
}
@ -115,13 +93,13 @@ func (r *scrobbleRepository) Count(options ...rest.QueryOptions) (int64, error)
func (r *scrobbleRepository) Get(id string) (*model.Scrobble, error) {
sel := r.baseQuery().Where(Eq{"row_id": id})
res := dbScrobble{}
var res dbScrobble
err := r.queryOne(sel, &res)
if err != nil {
return nil, err
}
model := res.toScrobble()
return &model, nil
return &model, err
}
func (r *scrobbleRepository) GetAll(options ...model.QueryOptions) (model.Scrobbles, error) {

View File

@ -104,9 +104,10 @@ var _ = Describe("ScrobbleRepository", func() {
It("returns an existing scrobble for the user", func() {
scrobble, err := repo.Get("1")
Expect(err).To(BeNil())
scrobble.MediaFile.CreatedAt = time.Time{}
Expect(scrobble.MediaFile).To(Equal(songDayInALife))
Expect(scrobble.SubmissionTime).To(BeTemporally("==", time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)))
Expect(scrobble.ID).To(Equal(int64(1)))
Expect(scrobble.MediaFileID).To(Equal("1001"))
Expect(scrobble.SubmissionTime).To(BeTemporally("==", firstScrobble.SubmissionTime))
})
It("does not return a scrobble that exists for another user", func() {
@ -131,12 +132,13 @@ var _ = Describe("ScrobbleRepository", func() {
Expect(err).To(BeNil())
Expect(scrobbles).To(HaveLen(2))
for idx := range scrobbles {
scrobbles[idx].MediaFile.CreatedAt = time.Time{}
}
Expect(scrobbles[0].ID).To(Equal(int64(3)))
Expect(scrobbles[0].MediaFileID).To(Equal("1002"))
Expect(scrobbles[0].SubmissionTime).To(BeTemporally("==", thirdScrobble.SubmissionTime))
Expect(scrobbles[1].MediaFile).To(Equal(songDayInALife))
Expect(scrobbles[0].MediaFile).To(Equal(songComeTogether))
Expect(scrobbles[1].ID).To(Equal(int64(1)))
Expect(scrobbles[1].MediaFileID).To(Equal("1001"))
Expect(scrobbles[1].SubmissionTime).To(BeTemporally("==", firstScrobble.SubmissionTime))
})
It("returns scrobbles in a range", func() {
@ -146,9 +148,9 @@ var _ = Describe("ScrobbleRepository", func() {
Expect(err).To(BeNil())
Expect(scrobbles).To(HaveLen(1))
scrobbles[0].MediaFile.CreatedAt = time.Time{}
Expect(scrobbles[0].MediaFile).To(Equal(songComeTogether))
Expect(scrobbles[0].ID).To(Equal(int64(3)))
Expect(scrobbles[0].MediaFileID).To(Equal("1002"))
Expect(scrobbles[0].SubmissionTime).To(BeTemporally("==", thirdScrobble.SubmissionTime))
})
})
})
@ -173,9 +175,9 @@ var _ = Describe("ScrobbleRepository", func() {
It("returns an existing scrobble for the user", func() {
scrobble, err := repo.Get("2")
Expect(err).To(BeNil())
scrobble.MediaFile.CreatedAt = time.Time{}
Expect(scrobble.MediaFile).To(Equal(songRadioactivity))
Expect(scrobble.SubmissionTime).To(BeTemporally("==", time.Date(1970, 2, 1, 0, 0, 0, 0, time.UTC)))
Expect(scrobble.ID).To(Equal(int64(2)))
Expect(scrobble.MediaFileID).To(Equal("1003"))
Expect(scrobble.SubmissionTime).To(BeTemporally("==", secondScrobble.SubmissionTime))
})
It("does not return a scrobble that exists for another user", func() {
@ -200,8 +202,9 @@ var _ = Describe("ScrobbleRepository", func() {
Expect(err).To(BeNil())
Expect(scrobbles).To(HaveLen(1))
scrobbles[0].MediaFile.CreatedAt = time.Time{}
Expect(scrobbles[0].MediaFile).To(Equal(songRadioactivity))
Expect(scrobbles[0].ID).To(Equal(int64(2)))
Expect(scrobbles[0].MediaFileID).To(Equal("1003"))
Expect(scrobbles[0].SubmissionTime).To(BeTemporally("==", secondScrobble.SubmissionTime))
})
It("returns scrobbles in a range", func() {
@ -211,8 +214,9 @@ var _ = Describe("ScrobbleRepository", func() {
Expect(err).To(BeNil())
Expect(scrobbles).To(HaveLen(1))
scrobbles[0].MediaFile.CreatedAt = time.Time{}
Expect(scrobbles[0].MediaFile).To(Equal(songRadioactivity))
Expect(scrobbles[0].ID).To(Equal(int64(2)))
Expect(scrobbles[0].MediaFileID).To(Equal("1003"))
Expect(scrobbles[0].SubmissionTime).To(BeTemporally("==", secondScrobble.SubmissionTime))
})
})
})

View File

@ -2,6 +2,7 @@ package tests
import (
"context"
"strconv"
"time"
"github.com/navidrome/navidrome/model"
@ -15,7 +16,7 @@ type MockScrobbleRepo struct {
func (m *MockScrobbleRepo) Get(id string) (*model.Scrobble, error) {
for _, scrobble := range m.RecordedScrobbles {
if scrobble.ID == id {
if strconv.FormatInt(scrobble.ID, 10) == id {
return &scrobble, nil
}
}