OpenSubsonic: playbackReport extension (reportPlayback), integrated with now-playing/history
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package subsonic
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerExtension("playbackReport", 1)
|
||||
register("reportPlayback", handleReportPlayback)
|
||||
}
|
||||
|
||||
// resolveMediaID maps a reportPlayback/getTranscodeDecision mediaId to a
|
||||
// song, honoring mediaType: "song" IDs are looked up directly; "podcast"
|
||||
// IDs may name either a podcast_episode (resolved to its downloaded
|
||||
// song, once one exists) or, since a downloaded episode's song ID is
|
||||
// itself usable everywhere, the song directly — both are accepted so
|
||||
// clients don't need to track which kind of ID they were given.
|
||||
func resolveMediaID(db *sql.DB, mediaID, mediaType string) (*database.Song, error) {
|
||||
if song, err := database.GetSong(db, mediaID); err == nil {
|
||||
return song, nil
|
||||
}
|
||||
if mediaType == "podcast" {
|
||||
if ep, err := database.GetPodcastEpisode(db, mediaID); err == nil && ep.MediaFileID.Valid {
|
||||
if song, err := database.GetSongByMediaFileID(db, ep.MediaFileID.String); err == nil {
|
||||
return song, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, database.ErrNotFound
|
||||
}
|
||||
|
||||
// handleReportPlayback records a client's playback timeline state. This
|
||||
// is deliberately separate from scrobble: it's meant for frequent,
|
||||
// low-stakes position pings (used to drive now-playing displays and
|
||||
// resume positions) rather than "this was a real listen" submission
|
||||
// events, though a state=stopped report with ignoreScrobble=false does
|
||||
// also log a playback_history entry, matching how a client naturally
|
||||
// signals "playback ended here" without a distinct submission call.
|
||||
func handleReportPlayback(rc *Context) *apiError {
|
||||
mediaID, aerr := rc.Params.Required("mediaId")
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
mediaType, aerr := rc.Params.Required("mediaType")
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
positionMs, aerr := rc.Params.RequiredInt("positionMs")
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
state, aerr := rc.Params.Required("state")
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
switch state {
|
||||
case "starting", "playing", "paused", "stopped":
|
||||
default:
|
||||
return newError(ErrGeneric, "invalid state: "+state)
|
||||
}
|
||||
playbackRate := 1.0
|
||||
if v := rc.Params.Get("playbackRate"); v != "" {
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
playbackRate = f
|
||||
}
|
||||
}
|
||||
ignoreScrobble := rc.Params.Bool("ignoreScrobble", false)
|
||||
client := rc.Params.Get("c")
|
||||
db := rc.Server.DB
|
||||
|
||||
song, err := resolveMediaID(db, mediaID, mediaType)
|
||||
if err != nil {
|
||||
return errNotFound("Media")
|
||||
}
|
||||
|
||||
if err := database.RecordPlaybackReport(db, rc.User.ID, song.ID, positionMs, state, client, playbackRate); err != nil {
|
||||
return newError(ErrGeneric, err.Error())
|
||||
}
|
||||
|
||||
switch state {
|
||||
case "stopped":
|
||||
if err := database.ClearNowPlaying(db, rc.User.ID); err != nil {
|
||||
return newError(ErrGeneric, err.Error())
|
||||
}
|
||||
if !ignoreScrobble && positionMs > 0 {
|
||||
if err := database.RecordPlayback(db, rc.User.ID, song.ID, float64(positionMs)/1000); err != nil {
|
||||
return newError(ErrGeneric, err.Error())
|
||||
}
|
||||
}
|
||||
default: // starting, playing, paused
|
||||
if err := database.SetNowPlaying(db, rc.User.ID, song.ID, client, float64(positionMs)/1000); err != nil {
|
||||
return newError(ErrGeneric, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
rc.Response = newResponse()
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user