feat(subsonic): update play stats when scrobbling
closes: #207 Co-authored-by: Brian Doherty <brian.r.doherty@gmail.com>
This commit is contained in:
@@ -55,6 +55,10 @@ func (c *Controller) ServeScrobble(r *http.Request) *spec.Response {
|
|||||||
optStamp := params.GetOrTime("time", time.Now())
|
optStamp := params.GetOrTime("time", time.Now())
|
||||||
optSubmission := params.GetOrBool("submission", true)
|
optSubmission := params.GetOrBool("submission", true)
|
||||||
|
|
||||||
|
if err := streamUpdateStats(c.DB, user.ID, track.Album.ID, optStamp); err != nil {
|
||||||
|
return spec.NewError(0, "error updating stats: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
var scrobbleErrs multierr.Err
|
var scrobbleErrs multierr.Err
|
||||||
for _, scrobbler := range c.Scrobblers {
|
for _, scrobbler := range c.Scrobblers {
|
||||||
if err := scrobbler.Scrobble(user, track, optStamp, optSubmission); err != nil {
|
if err := scrobbler.Scrobble(user, track, optStamp, optSubmission); err != nil {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
||||||
@@ -26,42 +27,64 @@ import (
|
|||||||
// b) return a non-nil spec.Response
|
// b) return a non-nil spec.Response
|
||||||
// _but not both_
|
// _but not both_
|
||||||
|
|
||||||
func streamGetTransPref(dbc *db.DB, userID int, client string) db.TranscodePreference {
|
func streamGetTransPref(dbc *db.DB, userID int, client string) (*db.TranscodePreference, error) {
|
||||||
pref := db.TranscodePreference{}
|
var pref db.TranscodePreference
|
||||||
dbc.
|
err := dbc.
|
||||||
Where("user_id=?", userID).
|
Where("user_id=?", userID).
|
||||||
Where("client COLLATE NOCASE IN (?)", []string{"*", client}).
|
Where("client COLLATE NOCASE IN (?)", []string{"*", client}).
|
||||||
Order("client DESC"). // ensure "*" is last if it's there
|
Order("client DESC"). // ensure "*" is last if it's there
|
||||||
First(&pref)
|
First(&pref).
|
||||||
return pref
|
Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return &pref, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("find transcode preference: %w", err)
|
||||||
|
}
|
||||||
|
return &pref, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamGetTrack(dbc *db.DB, trackID int) (*db.Track, error) {
|
func streamGetTrack(dbc *db.DB, trackID int) (*db.Track, error) {
|
||||||
track := db.Track{}
|
var track db.Track
|
||||||
err := dbc.
|
err := dbc.
|
||||||
Preload("Album").
|
Preload("Album").
|
||||||
First(&track, trackID).
|
First(&track, trackID).
|
||||||
Error
|
Error
|
||||||
return &track, err
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("find track: %w", err)
|
||||||
|
}
|
||||||
|
return &track, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamGetPodcast(dbc *db.DB, podcastID int) (*db.PodcastEpisode, error) {
|
func streamGetPodcast(dbc *db.DB, podcastID int) (*db.PodcastEpisode, error) {
|
||||||
podcast := db.PodcastEpisode{}
|
var podcast db.PodcastEpisode
|
||||||
err := dbc.First(&podcast, podcastID).Error
|
if err := dbc.First(&podcast, podcastID).Error; err != nil {
|
||||||
return &podcast, err
|
return nil, fmt.Errorf("find podcast: %w", err)
|
||||||
|
}
|
||||||
|
return &podcast, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamUpdateStats(dbc *db.DB, userID, albumID int) {
|
func streamUpdateStats(dbc *db.DB, userID, albumID int, playTime time.Time) error {
|
||||||
play := db.Play{
|
play := db.Play{
|
||||||
AlbumID: albumID,
|
AlbumID: albumID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
dbc.
|
err := dbc.
|
||||||
Where(play).
|
Where(play).
|
||||||
First(&play)
|
First(&play).
|
||||||
play.Time = time.Now() // for getAlbumList?type=recent
|
Error
|
||||||
play.Count++ // for getAlbumList?type=frequent
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
dbc.Save(&play)
|
return fmt.Errorf("find stat: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
play.Count++ // for getAlbumList?type=frequent
|
||||||
|
if playTime.After(play.Time) {
|
||||||
|
play.Time = playTime // for getAlbumList?type=recent
|
||||||
|
}
|
||||||
|
if err := dbc.Save(&play).Error; err != nil {
|
||||||
|
return fmt.Errorf("save stat: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -242,10 +265,18 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
|
|||||||
|
|
||||||
user := r.Context().Value(CtxUser).(*db.User)
|
user := r.Context().Value(CtxUser).(*db.User)
|
||||||
if track, ok := audioFile.(*db.Track); ok && track.Album != nil {
|
if track, ok := audioFile.(*db.Track); ok && track.Album != nil {
|
||||||
defer streamUpdateStats(c.DB, user.ID, track.Album.ID)
|
defer func() {
|
||||||
|
if err := streamUpdateStats(c.DB, user.ID, track.Album.ID, time.Now()); err != nil {
|
||||||
|
log.Printf("error updating listen stats: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
pref, err := streamGetTransPref(c.DB, user.ID, params.GetOr("c", ""))
|
||||||
|
if err != nil {
|
||||||
|
return spec.NewError(0, "failed to get transcode stream preference: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pref := streamGetTransPref(c.DB, user.ID, params.GetOr("c", ""))
|
|
||||||
onInvalidProfile := func() error {
|
onInvalidProfile := func() error {
|
||||||
log.Printf("serving raw `%s`\n", audioFile.AudioFilename())
|
log.Printf("serving raw `%s`\n", audioFile.AudioFilename())
|
||||||
w.Header().Set("Content-Type", audioFile.MIME())
|
w.Header().Set("Content-Type", audioFile.MIME())
|
||||||
|
|||||||
Reference in New Issue
Block a user