feat: finish the upload, playback, and test gaps from the docs
ci / api / api (push) Successful in 40s
ci / db / db (push) Successful in 1m3s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Failing after 1m48s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 46s
ci / ts / ts (packages + web) (push) Failing after 4m57s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 5m7s

Quality upgrades, batch covers, staging preview, and resume after reload are in the queue. Loudness and crossfade work on web and Android, and the documented checks now have suites.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mrleo1nidandCursor committed 2026-10-21 09:37:53 +03:00
1 parent 2b76060ea3
commit 066ba24834
61 files changed
+2818 -463

No files matched your search

+73 -12
View File
@@ -10,17 +10,22 @@ import (
"pipu/api/internal/api/gen"
"pipu/api/internal/auth"
"pipu/api/internal/platform/apperr"
"pipu/api/internal/platform/sign"
)
// Handler — эндпоинты очереди на добавление (часть gen.StrictServerInterface).
type Handler struct {
svc *Service
svc *Service
signer *sign.Signer
xAccel bool
}
// NewHandler создаёт обработчик.
func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} }
// NewHandler создаёт обработчик. signer подписывает ссылки прослушивания из staging.
func NewHandler(svc *Service, signer *sign.Signer, xAccel bool) *Handler {
return &Handler{svc: svc, signer: signer, xAccel: xAccel}
}
func fileDTO(ctx context.Context, f File) gen.UploadFile {
func (h *Handler) fileDTO(ctx context.Context, f File) gen.UploadFile {
out := gen.UploadFile{
Id: f.ID, BatchId: f.BatchID, BatchComment: f.BatchComment,
Author: gen.UserRef{Id: f.AuthorID, Username: f.AuthorName},
@@ -28,7 +33,7 @@ func fileDTO(ctx context.Context, f File) gen.UploadFile {
SizeBytes: f.Size, ReceivedBytes: f.Received, Format: f.Format, Codec: f.Codec, Bitrate: f.Bitrate,
SampleRate: f.SampleRate, BitDepth: f.BitDepth, DurationMs: f.DurationMs, Tags: f.Tags,
MetadataEdited: f.MetadataEdited, MatchedArtistId: f.MatchedArtistID, MatchedAlbumId: f.MatchedAlbumID,
DuplicateOfTrackId: f.DuplicateOfTrackID, PossibleDuplicate: f.PossibleDuplicate,
DuplicateOfTrackId: f.DuplicateOfTrackID, PossibleDuplicate: f.PossibleDuplicate, QualityUpgrade: f.QualityUpgrade,
TargetSourceId: f.TargetSourceID, TargetRelPath: f.TargetRelPath, StatusReason: f.StatusReason,
ReviewedAt: f.ReviewedAt, CreatedAt: f.CreatedAt, UpdatedAt: f.UpdatedAt,
}
@@ -47,6 +52,12 @@ func fileDTO(ctx context.Context, f File) gen.UploadFile {
u := TusURL(f.ID)
out.TusUrl = &u
}
if h.signer != nil && f.StagingPath != "" && f.Status.KeepsStaging() {
if p, ok := auth.PrincipalFrom(ctx); ok && auth.Can(p.Role, auth.PermReviewUploads) {
u := h.streamURL(f.ID)
out.StreamUrl = &u
}
}
return out
}
@@ -64,10 +75,10 @@ func metaFromDTO(m gen.UploadMeta) Meta {
}
}
func pageDTO(ctx context.Context, files []File, next string) gen.UploadFilePage {
func (h *Handler) pageDTO(ctx context.Context, files []File, next string) gen.UploadFilePage {
out := gen.UploadFilePage{Items: make([]gen.UploadFile, 0, len(files))}
for _, f := range files {
out.Items = append(out.Items, fileDTO(ctx, f))
out.Items = append(out.Items, h.fileDTO(ctx, f))
}
if next != "" {
out.NextCursor = &next
@@ -96,7 +107,7 @@ func deref[T any](p *T, def T) T {
func settingsDTO(s Settings) gen.UploadSettings {
return gen.UploadSettings{
QueueLimitBytes: s.QueueLimitBytes, PerUserLimitBytes: s.PerUserLimitBytes, MaxFileBytes: s.MaxFileBytes,
DefaultSourceId: s.DefaultSourceID,
DefaultSourceId: s.DefaultSourceID, AllowQualityUpgrade: s.AllowQualityUpgrade,
}
}
@@ -174,7 +185,7 @@ func (h *Handler) ListMyUploads(ctx context.Context, req gen.ListMyUploadsReques
if err != nil {
return nil, err
}
return gen.ListMyUploads200JSONResponse(pageDTO(ctx, files, next)), nil
return gen.ListMyUploads200JSONResponse(h.pageDTO(ctx, files, next)), nil
}
// WithdrawUpload реализует gen.StrictServerInterface.
@@ -195,7 +206,7 @@ func (h *Handler) ListUploads(ctx context.Context, req gen.ListUploadsRequestObj
if err != nil {
return nil, err
}
return gen.ListUploads200JSONResponse(pageDTO(ctx, files, next)), nil
return gen.ListUploads200JSONResponse(h.pageDTO(ctx, files, next)), nil
}
// UpdateUpload реализует gen.StrictServerInterface.
@@ -209,7 +220,7 @@ func (h *Handler) UpdateUpload(ctx context.Context, req gen.UpdateUploadRequestO
if err != nil {
return nil, err
}
return gen.UpdateUpload200JSONResponse(fileDTO(ctx, f)), nil
return gen.UpdateUpload200JSONResponse(h.fileDTO(ctx, f)), nil
}
// RetryUploadImport реализует gen.StrictServerInterface.
@@ -218,7 +229,7 @@ func (h *Handler) RetryUploadImport(ctx context.Context, req gen.RetryUploadImpo
if err != nil {
return nil, err
}
return gen.RetryUploadImport200JSONResponse(fileDTO(ctx, f)), nil
return gen.RetryUploadImport200JSONResponse(h.fileDTO(ctx, f)), nil
}
// ReviewUploads реализует gen.StrictServerInterface.
@@ -272,9 +283,59 @@ func (h *Handler) UpdateUploadSettings(ctx context.Context, req gen.UpdateUpload
QueueLimitBytes: b.QueueLimitBytes, MaxFileBytes: b.MaxFileBytes,
PerUserLimitBytes: b.PerUserLimitBytes, ClearPerUser: b.PerUserLimitBytes == nil,
DefaultSourceID: b.DefaultSourceId, ClearDefault: b.DefaultSourceId == nil,
AllowQualityUpgrade: b.AllowQualityUpgrade,
})
if err != nil {
return nil, err
}
return gen.UpdateUploadSettings200JSONResponse(settingsDTO(s)), nil
}
// UnseenUploadDecisions реализует gen.StrictServerInterface.
func (h *Handler) UnseenUploadDecisions(ctx context.Context, _ gen.UnseenUploadDecisionsRequestObject) (gen.UnseenUploadDecisionsResponseObject, error) {
n, err := h.svc.Unseen(ctx)
if err != nil {
return nil, err
}
return gen.UnseenUploadDecisions200JSONResponse{Count: n}, nil
}
// MarkUploadsSeen реализует gen.StrictServerInterface.
func (h *Handler) MarkUploadsSeen(ctx context.Context, _ gen.MarkUploadsSeenRequestObject) (gen.MarkUploadsSeenResponseObject, error) {
if err := h.svc.MarkSeen(ctx); err != nil {
return nil, err
}
return gen.MarkUploadsSeen204Response{}, nil
}
// UpdateUploadBatch реализует gen.StrictServerInterface.
func (h *Handler) UpdateUploadBatch(ctx context.Context, req gen.UpdateUploadBatchRequestObject) (gen.UpdateUploadBatchResponseObject, error) {
b := req.Body
in := BatchPatch{}
if b.Artist != nil {
in.Artist = b.Artist
}
if b.Album != nil {
in.Album = b.Album
}
if b.Year != nil {
in.SetYear = true
in.Year = b.Year
}
if b.CoverBase64 != nil {
if *b.CoverBase64 == "" {
in.ClearCover = true
} else {
raw, err := DecodeCover(*b.CoverBase64)
if err != nil {
return nil, err
}
in.Cover = raw
}
}
files, err := h.svc.UpdateBatch(ctx, req.Id, in)
if err != nil {
return nil, err
}
return gen.UpdateUploadBatch200JSONResponse{Items: h.pageDTO(ctx, files, "").Items}, nil
}