feat(subsonic): update track play stats on scrobble instead of stream
This commit is contained in:
@@ -7,12 +7,11 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.senan.xyz/gonic/db"
|
||||
"go.senan.xyz/gonic/jukebox"
|
||||
"go.senan.xyz/gonic/lastfm"
|
||||
"go.senan.xyz/gonic/podcasts"
|
||||
"go.senan.xyz/gonic/scrobble"
|
||||
"go.senan.xyz/gonic/server/ctrlbase"
|
||||
"go.senan.xyz/gonic/server/ctrlsubsonic/artistinfocache"
|
||||
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
||||
@@ -40,11 +39,6 @@ func PathsOf(paths []MusicPath) []string {
|
||||
return r
|
||||
}
|
||||
|
||||
type Scrobbler interface {
|
||||
IsUserAuthenticated(user *db.User) bool
|
||||
Scrobble(user *db.User, track *db.Track, stamp time.Time, submission bool) error
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
*ctrlbase.Controller
|
||||
MusicPaths []MusicPath
|
||||
@@ -52,7 +46,7 @@ type Controller struct {
|
||||
CacheAudioPath string
|
||||
CacheCoverPath string
|
||||
Jukebox *jukebox.Jukebox
|
||||
Scrobblers []Scrobbler
|
||||
Scrobblers []scrobble.Scrobbler
|
||||
Podcasts *podcasts.Podcasts
|
||||
Transcoder transcode.Transcoder
|
||||
LastFMClient *lastfm.Client
|
||||
|
||||
@@ -7,38 +7,22 @@ import (
|
||||
"math"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"go.senan.xyz/gonic/db"
|
||||
"go.senan.xyz/gonic/scanner"
|
||||
"go.senan.xyz/gonic/scrobble"
|
||||
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
||||
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
||||
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
|
||||
"go.senan.xyz/gonic/server/ctrlsubsonic/specidpaths"
|
||||
)
|
||||
|
||||
func lowerUDecOrHash(in string) string {
|
||||
lower := unicode.ToLower(rune(in[0]))
|
||||
if !unicode.IsLetter(lower) {
|
||||
return "#"
|
||||
}
|
||||
return string(lower)
|
||||
}
|
||||
|
||||
func getMusicFolder(musicPaths []MusicPath, p params.Params) string {
|
||||
idx, err := p.GetInt("musicFolderId")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if idx < 0 || idx >= len(musicPaths) {
|
||||
return ""
|
||||
}
|
||||
return musicPaths[idx].Path
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetLicence(_ *http.Request) *spec.Response {
|
||||
sub := spec.NewResponse()
|
||||
sub.Licence = &spec.Licence{
|
||||
@@ -56,28 +40,60 @@ func (c *Controller) ServeScrobble(r *http.Request) *spec.Response {
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
|
||||
id, err := params.GetID("id")
|
||||
if err != nil || id.Type != specid.Track {
|
||||
return spec.NewError(10, "please provide a track `id` track parameter")
|
||||
}
|
||||
|
||||
track := &db.Track{}
|
||||
if err := c.DB.Preload("Album").Preload("Album.Artists").First(track, id.Value).Error; err != nil {
|
||||
return spec.NewError(0, "error finding track: %v", err)
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide a `id` parameter")
|
||||
}
|
||||
|
||||
optStamp := params.GetOrTime("time", time.Now())
|
||||
optSubmission := params.GetOrBool("submission", true)
|
||||
|
||||
if err := streamUpdateStats(c.DB, user.ID, track, optStamp); err != nil {
|
||||
return spec.NewError(0, "error updating stats: %v", err)
|
||||
var scrobbleTrack scrobble.Track
|
||||
|
||||
switch id.Type {
|
||||
case specid.Track:
|
||||
var track db.Track
|
||||
if err := c.DB.Preload("Album").Preload("Album.Artists").First(&track, id.Value).Error; err != nil {
|
||||
return spec.NewError(0, "error finding track: %v", err)
|
||||
}
|
||||
if track.Album == nil {
|
||||
return spec.NewError(0, "track has no album %d", track.ID)
|
||||
}
|
||||
|
||||
scrobbleTrack.Track = track.TagTitle
|
||||
scrobbleTrack.Artist = track.TagTrackArtist
|
||||
scrobbleTrack.Album = track.Album.TagTitle
|
||||
scrobbleTrack.AlbumArtist = strings.Join(track.Album.ArtistsStrings(), ", ")
|
||||
scrobbleTrack.TrackNumber = uint(track.TagTrackNumber)
|
||||
scrobbleTrack.Duration = time.Second * time.Duration(track.Length)
|
||||
if _, err := uuid.Parse(track.TagBrainzID); err == nil {
|
||||
scrobbleTrack.MusicBrainzID = track.TagBrainzID
|
||||
}
|
||||
|
||||
if err := scrobbleStatsUpdateTrack(c.DB, &track, user.ID, optStamp); err != nil {
|
||||
return spec.NewError(0, "error updating stats: %v", err)
|
||||
}
|
||||
|
||||
case specid.PodcastEpisode:
|
||||
var podcastEpisode db.PodcastEpisode
|
||||
if err := c.DB.Preload("Podcast").First(&podcastEpisode, id.Value).Error; err != nil {
|
||||
return spec.NewError(0, "error finding podcast episode: %v", err)
|
||||
}
|
||||
|
||||
scrobbleTrack.Track = podcastEpisode.Title
|
||||
scrobbleTrack.Artist = podcastEpisode.Podcast.Title
|
||||
scrobbleTrack.Duration = time.Second * time.Duration(podcastEpisode.Length)
|
||||
|
||||
if err := scrobbleStatsUpdatePodcastEpisode(c.DB, id.Value); err != nil {
|
||||
return spec.NewError(0, "error updating stats: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var scrobbleErrs []error
|
||||
for _, scrobbler := range c.Scrobblers {
|
||||
if !scrobbler.IsUserAuthenticated(user) {
|
||||
if !scrobbler.IsUserAuthenticated(*user) {
|
||||
continue
|
||||
}
|
||||
if err := scrobbler.Scrobble(user, track, optStamp, optSubmission); err != nil {
|
||||
if err := scrobbler.Scrobble(*user, scrobbleTrack, optStamp, optSubmission); err != nil {
|
||||
scrobbleErrs = append(scrobbleErrs, err)
|
||||
}
|
||||
}
|
||||
@@ -426,3 +442,56 @@ func (c *Controller) ServeGetLyrics(_ *http.Request) *spec.Response {
|
||||
sub.Lyrics = &spec.Lyrics{}
|
||||
return sub
|
||||
}
|
||||
|
||||
func scrobbleStatsUpdateTrack(dbc *db.DB, track *db.Track, userID int, playTime time.Time) error {
|
||||
var play db.Play
|
||||
if err := dbc.Where("album_id=? AND user_id=?", track.AlbumID, userID).First(&play).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("find stat: %w", err)
|
||||
}
|
||||
|
||||
play.AlbumID = track.AlbumID
|
||||
play.UserID = userID
|
||||
play.Count++ // for getAlbumList?type=frequent
|
||||
play.Length += track.Length
|
||||
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
|
||||
}
|
||||
|
||||
func scrobbleStatsUpdatePodcastEpisode(dbc *db.DB, peID int) error {
|
||||
var pe db.PodcastEpisode
|
||||
if err := dbc.Where("id=?", peID).First(&pe).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("find podcast episode: %w", err)
|
||||
}
|
||||
|
||||
pe.ModifiedAt = time.Now()
|
||||
|
||||
if err := dbc.Save(&pe).Error; err != nil {
|
||||
return fmt.Errorf("save podcast episode: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getMusicFolder(musicPaths []MusicPath, p params.Params) string {
|
||||
idx, err := p.GetInt("musicFolderId")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if idx < 0 || idx >= len(musicPaths) {
|
||||
return ""
|
||||
}
|
||||
return musicPaths[idx].Path
|
||||
}
|
||||
|
||||
func lowerUDecOrHash(in string) string {
|
||||
lower := unicode.ToLower(rune(in[0]))
|
||||
if !unicode.IsLetter(lower) {
|
||||
return "#"
|
||||
}
|
||||
return string(lower)
|
||||
}
|
||||
|
||||
@@ -62,48 +62,6 @@ func streamGetTranscodeMeta(dbc *db.DB, userID int, client string) spec.Transcod
|
||||
}
|
||||
}
|
||||
|
||||
func streamUpdateStats(dbc *db.DB, userID int, track *db.Track, playTime time.Time) error {
|
||||
var play db.Play
|
||||
err := dbc.
|
||||
Where("album_id=? AND user_id=?", track.AlbumID, userID).
|
||||
First(&play).
|
||||
Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("find stat: %w", err)
|
||||
}
|
||||
|
||||
play.AlbumID = track.AlbumID
|
||||
play.UserID = userID
|
||||
play.Count++ // for getAlbumList?type=frequent
|
||||
play.Length += track.Length
|
||||
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
|
||||
}
|
||||
|
||||
func streamUpdatePodcastEpisodeStats(dbc *db.DB, peID int) error {
|
||||
var pe db.PodcastEpisode
|
||||
err := dbc.
|
||||
Where("id=?", peID).
|
||||
First(&pe).
|
||||
Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("find podcast episode: %w", err)
|
||||
}
|
||||
|
||||
pe.ModifiedAt = time.Now()
|
||||
|
||||
if err := dbc.Save(&pe).Error; err != nil {
|
||||
return fmt.Errorf("save podcast episode: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
coverDefaultSize = 600
|
||||
coverCacheFormat = "png"
|
||||
@@ -258,22 +216,6 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
|
||||
return spec.NewError(0, "type of id does not contain audio")
|
||||
}
|
||||
|
||||
if track, ok := audioFile.(*db.Track); ok && track.Album != nil {
|
||||
defer func() {
|
||||
if err := streamUpdateStats(c.DB, user.ID, track, time.Now()); err != nil {
|
||||
log.Printf("error updating track status: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if pe, ok := audioFile.(*db.PodcastEpisode); ok {
|
||||
defer func() {
|
||||
if err := streamUpdatePodcastEpisodeStats(c.DB, pe.ID); err != nil {
|
||||
log.Printf("error updating podcast episode status: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
maxBitRate, _ := params.GetInt("maxBitRate")
|
||||
format, _ := params.Get("format")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user