Files
pipu-music/services/api/internal/playback/events.go
T
mrleo1nid e158558da3
ci / module boundaries / boundaries (push) Successful in 6s
ci / api / api (push) Successful in 47s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / db / db (push) Successful in 1m4s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m30s
ci / ts / ts (packages + web) (push) Successful in 2m54s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m14s
refactor: isolate playback and upload use cases
2026-10-22 13:52:16 +03:00

89 lines
2.4 KiB
Go

package playback
import (
"context"
"time"
"github.com/google/uuid"
"pipu/api/internal/api/gen"
"pipu/api/internal/auth"
)
// EventService owns event acceptance and its transaction; HTTP only authenticates and maps DTOs.
type EventService struct {
store *Store
now func() time.Time
}
func (s EventService) Accept(ctx context.Context, p auth.Principal, events []gen.PlayEvent) (gen.PostEvents200JSONResponse, error) {
now := s.now()
ids := make([]uuid.UUID, 0, len(events))
for _, e := range events {
ids = append(ids, e.TrackId)
}
durations, err := s.store.TrackDurations(ctx, ids)
if err != nil {
return gen.PostEvents200JSONResponse{}, err
}
clientType := s.store.SessionClientType(ctx, p.SessionID)
out := gen.PostEvents200JSONResponse{}
tx, err := s.store.Begin(ctx)
if err != nil {
return gen.PostEvents200JSONResponse{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
for _, e := range events {
dur, known := durations[e.TrackId]
at := e.OccurredAt.UTC()
if !known || at.Before(now.Add(-maxEventAge)) {
out.Rejected++
continue
}
if at.After(now.Add(maxEventClockSkew)) {
at = now // часы клиента спешат
}
ev := Event{
ID: e.Id, Type: string(e.Type), TrackID: e.TrackId, OccurredAt: at, PositionMs: e.PositionMs,
PlayedMs: e.PlayedMs, ContextID: e.ContextId, WaveSessionID: e.WaveSessionId,
Offline: e.Offline != nil && *e.Offline,
}
if e.ContextType != nil {
ct := string(*e.ContextType)
ev.ContextType = &ct
}
inserted, err := s.store.InsertEvent(ctx, tx, p.UserID, clientType, ev)
if err != nil {
return gen.PostEvents200JSONResponse{}, err
}
if !inserted {
out.Duplicates++
continue
}
out.Accepted++
played := 0
if e.PlayedMs != nil {
played = *e.PlayedMs
}
o := Judge(ev.Type, played, dur)
if o != (Outcome{}) {
if err := s.store.ApplyOutcome(ctx, tx, p.UserID, ev, o); err != nil {
return gen.PostEvents200JSONResponse{}, err
}
}
if outcome := impressionOutcome(ev.Type, o); outcome != "" && ev.ContextType != nil && *ev.ContextType == "wave" &&
ev.ContextID != nil {
if sid, err := uuid.Parse(*ev.ContextID); err == nil {
if err := s.store.MarkImpression(ctx, tx, sid, ev.TrackID, outcome); err != nil {
return gen.PostEvents200JSONResponse{}, err
}
}
}
}
if err := tx.Commit(ctx); err != nil {
return gen.PostEvents200JSONResponse{}, err
}
return out, nil
}