package persistence
import (
"context"
"crypto/sha256"
"encoding/hex"
"regexp"
"strings"
. "github.com/Masterminds/squirrel"
"github.com/deluan/rest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/id"
"github.com/pocketbase/dbx"
)
type playerRepository struct {
sqlRepository
}
func NewPlayerRepository(db dbx.Builder) model.PlayerRepository {
r := &playerRepository{}
r.db = db
r.registerModel(&model.Player{}, map[string]filterFunc{
"name": containsFilter("player.name"),
"hasapikey": hasAPIKeyFilter,
})
r.setSortMappings(map[string]string{
"user_name": "username",
})
return r
}
func hasAPIKeyFilter(_ string, value any) Sqlizer {
if v, _ := value.(string); strings.EqualFold(v, "true") {
return NotEq{"player.api_key_hash": nil}
}
return Eq{"player.api_key_hash": nil}
}
func (r *playerRepository) Put(ctx context.Context, p *model.Player) error {
_, err := r.put(ctx, p.ID, p)
return err
}
func (r *playerRepository) selectPlayer(ctx context.Context, options ...model.QueryOptions) SelectBuilder {
return r.newSelect(ctx, options...).
Columns("player.*", "player.api_key_hash is not null as has_api_key").
Join("user ON player.user_id = user.id").
Columns("user.user_name username")
}
func (r *playerRepository) Get(ctx context.Context, id string) (*model.Player, error) {
sel := r.selectPlayer(ctx).Where(Eq{"player.id": id})
var res model.Player
err := r.queryOne(ctx, sel, &res)
return &res, err
}
func (r *playerRepository) FindMatch(ctx context.Context, userId, client, userAgent string) (*model.Player, error) {
sel := r.selectPlayer(ctx).Where(And{
Eq{"client": client},
Eq{"user_agent": userAgent},
Eq{"user_id": userId},
})
var res model.Player
err := r.queryOne(ctx, sel, &res)
return &res, err
}
func (r *playerRepository) newRestSelect(ctx context.Context, options ...model.QueryOptions) SelectBuilder {
s := r.selectPlayer(ctx, options...)
return s.Where(r.addRestriction(ctx))
}
func (r *playerRepository) CountByClient(ctx context.Context, options ...model.QueryOptions) (map[string]int64, error) {
sel := r.newSelect(ctx, options...).
Columns(
"case when client = 'NavidromeUI' then name else client end as player",
"count(*) as count",
).GroupBy("client")
var res []struct {
Player string
Count int64
}
err := r.queryAll(ctx, sel, &res)
if err != nil {
return nil, err
}
counts := make(map[string]int64, len(res))
for _, c := range res {
counts[c.Player] = c.Count
}
return counts, nil
}
func (r *playerRepository) CountAll(ctx context.Context, options ...model.QueryOptions) (int64, error) {
return r.count(ctx, r.newRestSelect(ctx), options...)
}
func (r *playerRepository) Count(ctx context.Context, options ...rest.QueryOptions) (int64, error) {
return r.CountAll(ctx, r.parseRestOptions(ctx, options...))
}
func (r *playerRepository) Read(ctx context.Context, id string) (*model.Player, error) {
sel := r.newRestSelect(ctx).Where(Eq{"player.id": id})
var res model.Player
err := r.queryOne(ctx, sel, &res)
return &res, err
}
func (r *playerRepository) ReadAll(ctx context.Context, options ...rest.QueryOptions) ([]model.Player, error) {
sel := r.newRestSelect(ctx, r.parseRestOptions(ctx, options...))
res := model.Players{}
err := r.queryAll(ctx, sel, &res)
return res, err
}
var apiKeyFormat = regexp.MustCompile(`^` + consts.APIKeyPrefix + `[0-9A-Za-z]{22}$`)
func apiKeyValidationError(msg string) error {
return &rest.ValidationError{Errors: map[string]string{"apiKey": msg}}
}
func validateAPIKey(key string) error {
if !apiKeyFormat.MatchString(key) {
return apiKeyValidationError("resources.player.validation.apiKeyFormat")
}
return nil
}
func (r *playerRepository) Save(ctx context.Context, t *model.Player) (string, error) {
u := loggedUser(ctx)
if t.UserId == "" && u.ID != invalidUserId {
t.UserId = u.ID
}
if t.UserId != u.ID {
return "", rest.ErrPermissionDenied
}
if t.APIKey == nil || *t.APIKey == "" {
return "", apiKeyValidationError("ra.validation.required")
}
if err := validateAPIKey(*t.APIKey); err != nil {
return "", err
}
values, err := toSQLArgs(t)
if err != nil {
return "", err
}
values["id"] = id.NewRandom()
values["api_key_hash"] = hashAPIKey(*t.APIKey)
_, err = r.executeSQL(ctx, Insert(r.tableName).SetMap(values))
if isUniqueViolation(err) {
return "", apiKeyValidationError("ra.validation.unique")
}
if err != nil {
return "", err
}
return values["id"].(string), nil
}
func (r *playerRepository) Update(ctx context.Context, id string, entity model.Player, cols ...string) error {
t := &entity
t.ID = id
if t.APIKey == nil {
return r.updateOwned(ctx, id, t, cols...)
}
return r.inTx(func(tx *playerRepository) error {
if err := tx.SetAPIKey(ctx, id, *t.APIKey); err != nil {
return err
}
return tx.updateOwned(ctx, id, t, cols...)
})
}
func (r *playerRepository) inTx(block func(tx *playerRepository) error) error {
conn, ok := r.db.(*dbx.DB)
if !ok {
return block(r)
}
return conn.Transactional(func(tx *dbx.Tx) error {
return block(NewPlayerRepository(tx).(*playerRepository))
})
}
func (r *playerRepository) Delete(ctx context.Context, ids ...string) error {
return r.deleteOwnedAll(ctx, ids...)
}
func hashAPIKey(key string) string {
sum := sha256.Sum256([]byte(key))
return hex.EncodeToString(sum[:])
}
func (r *playerRepository) FindByAPIKey(ctx context.Context, key string) (*model.Player, error) {
sel := r.selectPlayer(ctx).Where(Eq{"player.api_key_hash": hashAPIKey(key)})
var res model.Player
if err := r.queryOne(ctx, sel, &res); err != nil {
return nil, err
}
return &res, nil
}
func (r *playerRepository) SetAPIKey(ctx context.Context, playerID, key string) error {
if key == "" {
return r.updateOwnedRow(ctx, playerID, ownerOrAdmin, map[string]any{"api_key_hash": nil})
}
if err := validateAPIKey(key); err != nil {
return err
}
err := r.updateOwnedRow(ctx, playerID, ownerOnly, map[string]any{"api_key_hash": hashAPIKey(key)})
if isUniqueViolation(err) {
return apiKeyValidationError("ra.validation.unique")
}
return err
}
func isUniqueViolation(err error) bool {
return err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed")
}
var _ model.PlayerRepository = (*playerRepository)(nil)
var _ rest.Repository[model.Player] = (*playerRepository)(nil)
var _ rest.Persistable[model.Player] = (*playerRepository)(nil)