102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
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
|
|
}
|