feat(api): streaming, transcoding, events, history and likes (M3)
- tracks carry a stream_url signed for the user for 12 h and the user's reaction; /stream checks the signature, expiry and that the user is not blocked, then hands the file to nginx by X-Accel-Redirect (or serves it with Range support when X_ACCEL=false) - original by default; formats browsers can't play and explicit high/medium/low go through Opus transcoding, never upwards: a lossy source under the target bitrate stays original; transcodes are cached per file and size, deduplicated in flight, limited by TRANSCODE_CONCURRENCY and trimmed hourly by LRU to the size limit - /events takes up to 500 events with client UUIDs, drops repeats and events older than 30 days, counts plays (≥ half or 4 min, or finished), early skips and history entries (≥ 30 s) - history with clearing, likes and dislikes with cursors and totals, track reactions, playback state with last-write-wins, tracks batch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8b76aa3964
commit
c05e523cbe
16 files changed
+3913
-155
No files matched your search
@@ -28,12 +28,35 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// NewHandler создаёт обработчик.
|
||||
func NewHandler(svc *Service, store *Store, signer *sign.Signer, covers *Covers, xAccel bool) *Handler {
|
||||
return &Handler{DTO: DTO{Signer: signer}, svc: svc, store: store, covers: covers, xAccel: xAccel}
|
||||
func NewHandler(svc *Service, store *Store, dto DTO, covers *Covers, xAccel bool) *Handler {
|
||||
return &Handler{DTO: dto, svc: svc, store: store, covers: covers, xAccel: xAccel}
|
||||
}
|
||||
|
||||
// DTO переводит строки каталога в схемы API; подписывает ссылки на обложки.
|
||||
type DTO struct{ Signer *sign.Signer }
|
||||
type DTO struct {
|
||||
Signer *sign.Signer
|
||||
// Personal дополняет треки данными текущего пользователя (ссылка на стрим, реакция).
|
||||
Personal Personalizer
|
||||
}
|
||||
|
||||
// Personalizer заполняет персональные поля треков для пользователя из контекста запроса.
|
||||
type Personalizer interface {
|
||||
Personalize(ctx context.Context, rows []TrackRow, out []gen.Track) error
|
||||
}
|
||||
|
||||
// Tracks переводит треки в схему и добавляет персональные поля.
|
||||
func (d DTO) Tracks(ctx context.Context, rows []TrackRow) ([]gen.Track, error) {
|
||||
out := make([]gen.Track, 0, len(rows))
|
||||
for _, t := range rows {
|
||||
out = append(out, d.Track(t))
|
||||
}
|
||||
if d.Personal != nil && len(out) > 0 {
|
||||
if err := d.Personal.Personalize(ctx, rows, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (h *Handler) sourceDTO(s Source) gen.Source {
|
||||
out := gen.Source{
|
||||
@@ -382,11 +405,13 @@ func (h *Handler) GetAlbum(ctx context.Context, req gen.GetAlbumRequestObject) (
|
||||
}
|
||||
out := gen.GetAlbum200JSONResponse{
|
||||
Id: a.ID, Title: a.Title, Year: a.Year, CoverUrl: h.coverURL(a), CoverColors: colors(a.CoverColors),
|
||||
Artist: gen.ArtistRef{Id: a.Artist.ID, Name: a.Artist.Name}, Tracks: make([]gen.Track, 0, len(tracks)),
|
||||
Artist: gen.ArtistRef{Id: a.Artist.ID, Name: a.Artist.Name},
|
||||
}
|
||||
for _, t := range tracks {
|
||||
out.DurationMs += t.DurationMs
|
||||
out.Tracks = append(out.Tracks, h.Track(t))
|
||||
}
|
||||
if out.Tracks, err = h.Tracks(ctx, tracks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -397,7 +422,11 @@ func (h *Handler) GetTrack(ctx context.Context, req gen.GetTrackRequestObject) (
|
||||
if err != nil {
|
||||
return nil, catalogErr(err)
|
||||
}
|
||||
return gen.GetTrack200JSONResponse(h.Track(t)), nil
|
||||
dto, err := h.Tracks(ctx, []TrackRow{t})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gen.GetTrack200JSONResponse(dto[0]), nil
|
||||
}
|
||||
|
||||
// GetCover реализует gen.StrictServerInterface: проверяет подпись и отдаёт WebP нужного размера.
|
||||
|
||||
Reference in new issue
Block a user