mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
Merge 6af63814af11f86e153b692c076cb86d77e8cbdb into 6b9f85efcc42aab1156ada175a18390f5cb0fbb2
This commit is contained in:
commit
4ba8263fae
@ -8,6 +8,12 @@ type Scrobble struct {
|
||||
SubmissionTime time.Time
|
||||
}
|
||||
|
||||
type MostPlayedEntry struct {
|
||||
MediaFile
|
||||
PlayCount int `json:"playCount"`
|
||||
}
|
||||
|
||||
type ScrobbleRepository interface {
|
||||
RecordScrobble(mediaFileID string, submissionTime time.Time) error
|
||||
GetMostPlayed(offset, count int) ([]MostPlayedEntry, error)
|
||||
}
|
||||
|
||||
@ -32,3 +32,49 @@ func (r *scrobbleRepository) RecordScrobble(mediaFileID string, submissionTime t
|
||||
_, err := r.executeSQL(insert)
|
||||
return err
|
||||
}
|
||||
|
||||
type dbMostPlayedEntry struct {
|
||||
dbMediaFile
|
||||
PlayCount int `structs:"-"`
|
||||
}
|
||||
|
||||
func (e *dbMostPlayedEntry) PostScan() error {
|
||||
return e.dbMediaFile.PostScan()
|
||||
}
|
||||
|
||||
func (r *scrobbleRepository) GetMostPlayed(offset, count int) ([]model.MostPlayedEntry, error) {
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if count <= 0 {
|
||||
count = 50
|
||||
}
|
||||
userID := loggedUser(r.ctx).ID
|
||||
sq := Select("m.*", "count(*) as play_count").
|
||||
From(r.tableName+" s").
|
||||
LeftJoin("media_file m ON m.id = s.media_file_id").
|
||||
Where(Eq{"s.user_id": userID}).
|
||||
GroupBy("m.id").
|
||||
OrderBy("play_count DESC").
|
||||
Offset(uint64(offset)).
|
||||
Limit(uint64(count))
|
||||
|
||||
var rows []dbMostPlayedEntry
|
||||
if err := r.queryAll(sq, &rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries := make([]model.MostPlayedEntry, 0, len(rows))
|
||||
for i := range rows {
|
||||
if rows[i].MediaFile == nil {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, model.MostPlayedEntry{
|
||||
MediaFile: *rows[i].MediaFile,
|
||||
PlayCount: rows[i].PlayCount,
|
||||
})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
var _ model.ScrobbleRepository = (*scrobbleRepository)(nil)
|
||||
|
||||
@ -228,6 +228,30 @@ func (api *Router) GetNowPlaying(r *http.Request) (*responses.Subsonic, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (api *Router) GetMostPlayedSongs(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).GetMostPlayed(offset, count)
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving most played songs", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := newResponse()
|
||||
response.MostPlayed = &responses.MostPlayed{
|
||||
Song: slice.Map(entries, func(e model.MostPlayedEntry) responses.MostPlayedEntry {
|
||||
return responses.MostPlayedEntry{
|
||||
Child: childFromMediaFile(ctx, e.MediaFile),
|
||||
PlayCount: e.PlayCount,
|
||||
}
|
||||
}),
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (api *Router) GetRandomSongs(r *http.Request) (*responses.Subsonic, error) {
|
||||
p := req.Params(r)
|
||||
size := min(p.IntOr("size", 10), 500)
|
||||
|
||||
@ -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, "getMostPlayedSongs", api.GetMostPlayedSongs)
|
||||
h(r, "getRandomSongs", api.GetRandomSongs)
|
||||
h(r, "getSongsByGenre", api.GetSongsByGenre)
|
||||
})
|
||||
|
||||
@ -15,6 +15,7 @@ func (api *Router) GetOpenSubsonicExtensions(_ *http.Request) (*responses.Subson
|
||||
{Name: "indexBasedQueue", Versions: []int32{1}},
|
||||
{Name: "transcoding", Versions: []int32{1}},
|
||||
{Name: "playbackReport", Versions: []int32{1}},
|
||||
{Name: "mostPlayedSongs", Versions: []int32{1}},
|
||||
}
|
||||
if api.sonic != nil && api.sonic.HasProvider() {
|
||||
extensions = append(extensions, responses.OpenSubsonicExtension{
|
||||
|
||||
@ -44,43 +44,13 @@ var _ = Describe("GetOpenSubsonicExtensions", func() {
|
||||
router = subsonic.New(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
})
|
||||
|
||||
It("should return the base 6 OpenSubsonicExtensions without sonicSimilarity", func() {
|
||||
It("should return the base 7 OpenSubsonicExtensions without sonicSimilarity", func() {
|
||||
router.ServeHTTP(w, r)
|
||||
|
||||
// Make sure the endpoint is public, by not passing any authentication
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(w.Header().Get("Content-Type")).To(Equal("application/json"))
|
||||
|
||||
var response responses.JsonWrapper
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(*response.Subsonic.OpenSubsonicExtensions).To(SatisfyAll(
|
||||
HaveLen(6),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "transcodeOffset", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "formPost", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "songLyrics", Versions: []int32{1, 2}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "indexBasedQueue", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "transcoding", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "playbackReport", Versions: []int32{1}}),
|
||||
))
|
||||
Expect(*response.Subsonic.OpenSubsonicExtensions).NotTo(
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "sonicSimilarity", Versions: []int32{1}}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Context("with sonic similarity plugin", func() {
|
||||
BeforeEach(func() {
|
||||
sonicService := sonicsvc.New(nil, &mockSonicPluginLoader{names: []string{"test-plugin"}}, nil)
|
||||
router = subsonic.New(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, sonicService)
|
||||
})
|
||||
|
||||
It("should return 7 extensions including sonicSimilarity", func() {
|
||||
router.ServeHTTP(w, r)
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(w.Header().Get("Content-Type")).To(Equal("application/json"))
|
||||
|
||||
var response responses.JsonWrapper
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -92,6 +62,38 @@ var _ = Describe("GetOpenSubsonicExtensions", func() {
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "indexBasedQueue", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "transcoding", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "playbackReport", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "mostPlayedSongs", Versions: []int32{1}}),
|
||||
))
|
||||
Expect(*response.Subsonic.OpenSubsonicExtensions).NotTo(
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "sonicSimilarity", Versions: []int32{1}}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Context("with sonic similarity plugin", func() {
|
||||
BeforeEach(func() {
|
||||
sonicService := sonicsvc.New(nil, &mockSonicPluginLoader{names: []string{"test-plugin"}}, nil)
|
||||
router = subsonic.New(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, sonicService)
|
||||
})
|
||||
|
||||
It("should return 8 extensions including sonicSimilarity", func() {
|
||||
router.ServeHTTP(w, r)
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(w.Header().Get("Content-Type")).To(Equal("application/json"))
|
||||
|
||||
var response responses.JsonWrapper
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(*response.Subsonic.OpenSubsonicExtensions).To(SatisfyAll(
|
||||
HaveLen(8),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "transcodeOffset", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "formPost", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "songLyrics", Versions: []int32{1, 2}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "indexBasedQueue", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "transcoding", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "playbackReport", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "mostPlayedSongs", Versions: []int32{1}}),
|
||||
ContainElement(responses.OpenSubsonicExtension{Name: "sonicSimilarity", Versions: []int32{1}}),
|
||||
))
|
||||
})
|
||||
|
||||
@ -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"`
|
||||
MostPlayed *MostPlayed `xml:"mostPlayed,omitempty" json:"mostPlayed,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
@ -448,6 +449,15 @@ type TopSongs struct {
|
||||
Song []Child `xml:"song,omitempty" json:"song,omitempty"`
|
||||
}
|
||||
|
||||
type MostPlayedEntry struct {
|
||||
Child
|
||||
PlayCount int `xml:"playCount,attr" json:"playCount"`
|
||||
}
|
||||
|
||||
type MostPlayed struct {
|
||||
Song []MostPlayedEntry `xml:"song,omitempty" json:"song,omitempty"`
|
||||
}
|
||||
|
||||
type SonicMatch struct {
|
||||
Entry Child `xml:"entry" json:"entry"`
|
||||
Similarity float64 `xml:"similarity" json:"similarity"`
|
||||
|
||||
@ -22,3 +22,7 @@ func (m *MockScrobbleRepo) RecordScrobble(fileID string, submissionTime time.Tim
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockScrobbleRepo) GetMostPlayed(offset, count int) ([]model.MostPlayedEntry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user