diff --git a/model/scrobble.go b/model/scrobble.go index e1567abc3..dc9113c8f 100644 --- a/model/scrobble.go +++ b/model/scrobble.go @@ -8,6 +8,12 @@ type Scrobble struct { SubmissionTime time.Time } +type HistoryEntry struct { + MediaFile + PlayedAt time.Time `json:"playedAt"` +} + type ScrobbleRepository interface { RecordScrobble(mediaFileID string, submissionTime time.Time) error + GetHistory(offset, count int) ([]HistoryEntry, error) } diff --git a/persistence/scrobble_repository.go b/persistence/scrobble_repository.go index 219a48198..1a2f40837 100644 --- a/persistence/scrobble_repository.go +++ b/persistence/scrobble_repository.go @@ -32,3 +32,48 @@ func (r *scrobbleRepository) RecordScrobble(mediaFileID string, submissionTime t _, err := r.executeSQL(insert) return err } + +type dbHistoryEntry struct { + dbMediaFile + SubmissionTime int64 `structs:"-"` +} + +func (h *dbHistoryEntry) PostScan() error { + return h.dbMediaFile.PostScan() +} + +func (r *scrobbleRepository) GetHistory(offset, count int) ([]model.HistoryEntry, error) { + if offset < 0 { + offset = 0 + } + if count <= 0 { + count = 50 + } + userID := loggedUser(r.ctx).ID + sq := Select("m.*", "s.submission_time"). + From(r.tableName+" s"). + LeftJoin("media_file m ON m.id = s.media_file_id"). + Where(Eq{"s.user_id": userID}). + OrderBy("s.submission_time DESC"). + Offset(uint64(offset)). + Limit(uint64(count)) + + var rows []dbHistoryEntry + if err := r.queryAll(sq, &rows); err != nil { + return nil, err + } + + entries := make([]model.HistoryEntry, 0, len(rows)) + for i := range rows { + if rows[i].MediaFile == nil { + continue + } + entries = append(entries, model.HistoryEntry{ + MediaFile: *rows[i].MediaFile, + PlayedAt: time.Unix(rows[i].SubmissionTime, 0), + }) + } + return entries, nil +} + +var _ model.ScrobbleRepository = (*scrobbleRepository)(nil) diff --git a/server/subsonic/album_lists.go b/server/subsonic/album_lists.go index 24bbca960..37e12b6f5 100644 --- a/server/subsonic/album_lists.go +++ b/server/subsonic/album_lists.go @@ -228,6 +228,30 @@ func (api *Router) GetNowPlaying(r *http.Request) (*responses.Subsonic, error) { return response, nil } +func (api *Router) GetSongHistory(r *http.Request) (*responses.Subsonic, error) { + p := req.Params(r) + count := min(p.IntOr("count", 50), 500) + offset := p.IntOr("offset", 0) + + ctx := r.Context() + entries, err := api.ds.Scrobble(ctx).GetHistory(offset, count) + if err != nil { + log.Error(r, "Error retrieving song history", err) + return nil, err + } + + response := newResponse() + response.SongHistory = &responses.SongHistory{ + Song: slice.Map(entries, func(e model.HistoryEntry) responses.SongHistoryEntry { + return responses.SongHistoryEntry{ + Child: childFromMediaFile(ctx, e.MediaFile), + PlayedAt: e.PlayedAt.Unix(), + } + }), + } + return response, nil +} + func (api *Router) GetRandomSongs(r *http.Request) (*responses.Subsonic, error) { p := req.Params(r) size := min(p.IntOr("size", 10), 500) diff --git a/server/subsonic/api.go b/server/subsonic/api.go index 82e404228..2d43a44da 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -136,6 +136,7 @@ func (api *Router) routes() http.Handler { h(r, "getStarred", api.GetStarred) h(r, "getStarred2", api.GetStarred2) h(r, "getNowPlaying", api.GetNowPlaying) + h(r, "getSongHistory", api.GetSongHistory) h(r, "getRandomSongs", api.GetRandomSongs) h(r, "getSongsByGenre", api.GetSongsByGenre) }) diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index 252eee4c6..c4fdaf171 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -63,6 +63,7 @@ type Subsonic struct { PlayQueueByIndex *PlayQueueByIndex `xml:"playQueueByIndex,omitempty" json:"playQueueByIndex,omitempty"` TranscodeDecision *TranscodeDecision `xml:"transcodeDecision,omitempty" json:"transcodeDecision,omitempty"` SonicMatches *Array[SonicMatch] `xml:"sonicMatch,omitempty" json:"sonicMatch,omitempty"` + SongHistory *SongHistory `xml:"songHistory,omitempty" json:"songHistory,omitempty"` } const ( @@ -374,6 +375,15 @@ type NowPlaying struct { Entry []NowPlayingEntry `xml:"entry" json:"entry,omitempty"` } +type SongHistoryEntry struct { + Child + PlayedAt int64 `xml:"playedAt,attr" json:"playedAt"` +} + +type SongHistory struct { + Song []SongHistoryEntry `xml:"song,omitempty" json:"song,omitempty"` +} + type User struct { Username string `xml:"username,attr" json:"username"` Email string `xml:"email,attr,omitempty" json:"email,omitempty"` diff --git a/tests/mock_scrobble_repo.go b/tests/mock_scrobble_repo.go index 34561c257..416ffe89f 100644 --- a/tests/mock_scrobble_repo.go +++ b/tests/mock_scrobble_repo.go @@ -22,3 +22,7 @@ func (m *MockScrobbleRepo) RecordScrobble(fileID string, submissionTime time.Tim }) return nil } + +func (m *MockScrobbleRepo) GetHistory(offset, count int) ([]model.HistoryEntry, error) { + return nil, nil +}