package core
import (
"context"
"fmt"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/id"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils"
)
type Players interface {
Get(ctx context.Context, playerId string) (*model.Player, error)
Register(ctx context.Context, id, client, userAgent, ip string) (*model.Player, *model.Transcoding, error)
Touch(ctx context.Context, plr model.Player, client, userAgent, ip string) (*model.Player, *model.Transcoding, error)
}
func NewPlayers(ds model.DataStore) Players {
return &players{
ds: ds,
limiter: utils.Limiter{Interval: consts.UpdatePlayerFrequency},
}
}
type players struct {
ds model.DataStore
limiter utils.Limiter
}
func (p *players) Register(ctx context.Context, playerID, client, userAgent, ip string) (*model.Player, *model.Transcoding, error) {
var plr *model.Player
var err error
user, _ := request.UserFrom(ctx)
if playerID != "" {
plr, err = p.ds.Player().Get(ctx, playerID)
if err == nil && (plr.Client != client || plr.UserId != user.ID) {
playerID = ""
}
}
username := userName(ctx)
if err != nil || playerID == "" {
plr, err = p.ds.Player().FindMatch(ctx, user.ID, client, userAgent)
if err == nil {
log.Debug(ctx, "Found matching player", "id", plr.ID, "client", client, "username", username, "type", userAgent)
} else {
plr = &model.Player{
ID: id.NewRandom(),
UserId: user.ID,
Client: client,
ScrobbleEnabled: true,
ReportRealPath: conf.Server.Subsonic.DefaultReportRealPath,
}
log.Info(ctx, "Registering new player", "id", plr.ID, "client", client, "username", username, "type", userAgent)
}
}
if !plr.HasAPIKey {
plr.Name = fmt.Sprintf("%s [%s]", client, userAgent)
}
return p.refresh(ctx, plr, userAgent, ip)
}
func (p *players) Touch(ctx context.Context, plr model.Player, client, userAgent, ip string) (*model.Player, *model.Transcoding, error) {
if plr.Client == "" {
plr.Client = client
}
return p.refresh(ctx, &plr, userAgent, ip)
}
func (p *players) refresh(ctx context.Context, plr *model.Player, userAgent, ip string) (*model.Player, *model.Transcoding, error) {
plr.UserAgent = userAgent
plr.IP = ip
plr.LastSeen = time.Now()
p.limiter.Do(plr.ID, func() {
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
if err := p.ds.Player().Put(ctx, plr); err != nil {
log.Warn(ctx, "Could not save player", "id", plr.ID, "client", plr.Client, "username", userName(ctx), "type", plr.UserAgent, err)
}
})
if plr.TranscodingId == "" {
return plr, nil, nil
}
trc, err := p.ds.Transcoding().Get(ctx, plr.TranscodingId)
return plr, trc, err
}
func (p *players) Get(ctx context.Context, playerId string) (*model.Player, error) {
return p.ds.Player().Get(ctx, playerId)
}