package uploads import ( "context" "errors" "net/http" "github.com/google/uuid" "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 signer *sign.Signer } // NewHandler создаёт обработчик. signer подписывает ссылки прослушивания из staging. func NewHandler(svc *Service, signer *sign.Signer) *Handler { return &Handler{svc: svc, signer: signer} } func (h *Handler) fileDTO(ctx context.Context, f File) gen.UploadFile { hasCover := f.CoverPath != nil && *f.CoverPath != "" out := gen.UploadFile{ BatchHasCover: &hasCover, Id: f.ID, BatchId: f.BatchID, BatchComment: f.BatchComment, Author: gen.UserRef{Id: f.AuthorID, Username: f.AuthorName}, Status: gen.UploadStatus(f.Status), OriginalName: f.OriginalName, ClientRelPath: f.ClientRelPath, 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, QualityUpgrade: f.QualityUpgrade, TargetSourceId: f.TargetSourceID, TargetRelPath: f.TargetRelPath, StatusReason: f.StatusReason, TargetSourceName: f.TargetSourceName, TargetStoragePath: f.TargetStoragePath, ReviewedAt: f.ReviewedAt, CreatedAt: f.CreatedAt, UpdatedAt: f.UpdatedAt, } if out.Tags == nil { out.Tags = map[string]string{} } if f.DuplicateLevel != nil { l := gen.UploadFileDuplicateLevel(*f.DuplicateLevel) out.DuplicateLevel = &l } if f.Meta != nil { m := metaDTO(*f.Meta) out.Metadata = &m } if p, ok := auth.PrincipalFrom(ctx); ok && p.UserID == f.AuthorID && f.Status == StatusUploading { u := TusURL(f.ID) out.TusUrl = &u } if h.signer != nil && f.StagingPath != "" && f.Status.KeepsStaging() { if p, ok := auth.PrincipalFrom(ctx); ok && p.ReviewsUploads { u := h.streamURL(f.ID) out.StreamUrl = &u } } return out } func metaDTO(m Meta) gen.UploadMeta { return gen.UploadMeta{ Artist: m.Artist, Albumartist: m.AlbumArtist, Album: m.Album, Title: m.Title, Genre: m.Genre, Year: m.Year, Track: m.Track, Disc: m.Disc, } } func metaFromDTO(m gen.UploadMeta) Meta { return Meta{ Artist: m.Artist, AlbumArtist: m.Albumartist, Album: m.Album, Title: m.Title, Genre: m.Genre, Year: m.Year, Track: m.Track, Disc: m.Disc, } } 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, h.fileDTO(ctx, f)) } if next != "" { out.NextCursor = &next } return out } func statuses(in *gen.UploadStatusFilter) []Status { if in == nil { return nil } out := make([]Status, 0, len(*in)) for _, s := range *in { out = append(out, Status(s)) } return out } func deref[T any](p *T, def T) T { if p == nil { return def } return *p } func settingsDTO(s Settings) gen.UploadSettings { return gen.UploadSettings{ QueueLimitBytes: s.QueueLimitBytes, PerUserLimitBytes: s.PerUserLimitBytes, MaxFileBytes: s.MaxFileBytes, DefaultSourceId: s.DefaultSourceID, AllowQualityUpgrade: s.AllowQualityUpgrade, } } // GetUploadQuota реализует gen.StrictServerInterface. func (h *Handler) GetUploadQuota(ctx context.Context, _ gen.GetUploadQuotaRequestObject) (gen.GetUploadQuotaResponseObject, error) { q, err := h.svc.Quota(ctx) if err != nil { return nil, err } return gen.GetUploadQuota200JSONResponse{ LimitBytes: q.QueueLimitBytes, UsedBytes: q.UsedBytes, PerUserLimitBytes: q.PerUserLimitBytes, UserUsedBytes: q.UserUsedBytes, MaxFileBytes: q.MaxFileBytes, }, nil } // PrecheckUploads реализует gen.StrictServerInterface. func (h *Handler) PrecheckUploads(ctx context.Context, req gen.PrecheckUploadsRequestObject) (gen.PrecheckUploadsResponseObject, error) { in := make([]PrecheckFile, 0, len(req.Body.Files)) for _, f := range req.Body.Files { in = append(in, PrecheckFile{ ClientID: f.ClientId, Size: f.Size, SHA256: f.Sha256, Artist: deref(f.Artist, ""), Title: deref(f.Title, ""), DurationMs: deref(f.DurationMs, 0), }) } res, err := h.svc.Precheck(ctx, in) if err != nil { return nil, err } out := gen.PrecheckUploads200JSONResponse{} for _, r := range res { out.Files = append(out.Files, struct { ClientId string `json:"client_id"` PossibleDuplicate bool `json:"possible_duplicate"` Result gen.UploadPrecheckResultFilesResult `json:"result"` TrackId *uuid.UUID `json:"track_id"` }{ClientId: r.ClientID, PossibleDuplicate: r.Possible, Result: gen.UploadPrecheckResultFilesResult(r.Result), TrackId: r.Track}) } return out, nil } // CreateUploadBatch реализует gen.StrictServerInterface. func (h *Handler) CreateUploadBatch(ctx context.Context, req gen.CreateUploadBatchRequestObject) (gen.CreateUploadBatchResponseObject, error) { in := make([]CreateFile, 0, len(req.Body.Files)) for _, f := range req.Body.Files { in = append(in, CreateFile{ClientID: f.ClientId, Name: f.Name, RelPath: f.RelPath, Size: f.Size, SHA256: f.Sha256}) } var cover []byte if req.Body.CoverBase64 != nil { var err error cover, err = DecodeCover(*req.Body.CoverBase64) if err != nil { return nil, err } } batch, res, err := h.svc.Create(ctx, req.Body.Comment, in, cover) if err != nil { return nil, err } out := gen.CreateUploadBatch201JSONResponse{BatchId: batch} for _, r := range res { item := struct { ClientId string `json:"client_id"` Rejected *string `json:"rejected"` TrackId *uuid.UUID `json:"track_id"` TusUrl *string `json:"tus_url"` UploadFileId *uuid.UUID `json:"upload_file_id"` }{ClientId: r.ClientID, TrackId: r.Track} if r.Rejected != "" { code := r.Rejected item.Rejected = &code } else { id, u := r.FileID, TusURL(r.FileID) item.UploadFileId, item.TusUrl = &id, &u } out.Files = append(out.Files, item) } return out, nil } // ListMyUploads реализует gen.StrictServerInterface. func (h *Handler) ListMyUploads(ctx context.Context, req gen.ListMyUploadsRequestObject) (gen.ListMyUploadsResponseObject, error) { files, next, err := h.svc.ListMine(ctx, statuses(req.Params.Status), deref(req.Params.Cursor, ""), deref(req.Params.Limit, 50), req.Params.Batch) if err != nil { return nil, err } return gen.ListMyUploads200JSONResponse(h.pageDTO(ctx, files, next)), nil } // WithdrawUpload реализует gen.StrictServerInterface. func (h *Handler) WithdrawUpload(ctx context.Context, req gen.WithdrawUploadRequestObject) (gen.WithdrawUploadResponseObject, error) { if err := h.svc.Withdraw(ctx, req.Id); err != nil { return nil, err } return gen.WithdrawUpload204Response{}, nil } // ListUploads реализует gen.StrictServerInterface. func (h *Handler) ListUploads(ctx context.Context, req gen.ListUploadsRequestObject) (gen.ListUploadsResponseObject, error) { p := req.Params files, next, err := h.svc.ListAll(ctx, ListFilter{ AuthorID: p.Author, BatchID: p.Batch, Statuses: statuses(p.Status), PossibleDuplicate: p.PossibleDuplicate, Cursor: deref(p.Cursor, ""), Limit: deref(p.Limit, 50), }) if err != nil { return nil, err } return gen.ListUploads200JSONResponse(h.pageDTO(ctx, files, next)), nil } // UpdateUpload реализует gen.StrictServerInterface. func (h *Handler) UpdateUpload(ctx context.Context, req gen.UpdateUploadRequestObject) (gen.UpdateUploadResponseObject, error) { in := UpdateInput{TargetSourceID: req.Body.TargetSourceId} if req.Body.Metadata != nil { m := metaFromDTO(*req.Body.Metadata) in.Meta = &m } f, err := h.svc.Update(ctx, req.Id, in) if err != nil { return nil, err } return gen.UpdateUpload200JSONResponse(h.fileDTO(ctx, f)), nil } // RetryUploadImport реализует gen.StrictServerInterface. func (h *Handler) RetryUploadImport(ctx context.Context, req gen.RetryUploadImportRequestObject) (gen.RetryUploadImportResponseObject, error) { f, err := h.svc.Retry(ctx, req.Id) if err != nil { return nil, err } return gen.RetryUploadImport200JSONResponse(h.fileDTO(ctx, f)), nil } // ReviewUploads реализует gen.StrictServerInterface. func (h *Handler) ReviewUploads(ctx context.Context, req gen.ReviewUploadsRequestObject) (gen.ReviewUploadsResponseObject, error) { res, err := h.svc.Review(ctx, req.Body.FileIds, req.Body.Decision == "approve", deref(req.Body.Reason, "")) if err != nil { return nil, err } out := gen.ReviewUploads200JSONResponse{} for _, r := range res { item := struct { Error *gen.Problem `json:"error"` Id uuid.UUID `json:"id"` Ok bool `json:"ok"` }{Id: r.ID, Ok: r.Error == nil} if r.Error != nil { item.Error = problem(r.Error) } out.Items = append(out.Items, item) } return out, nil } // problem — ошибка решения по одному файлу в виде Problem (массовое действие не падает целиком). func problem(err error) *gen.Problem { var ae *apperr.Error if !errors.As(err, &ae) { return &gen.Problem{Status: http.StatusInternalServerError, Code: "common.internal"} } p := &gen.Problem{Status: ae.Status, Code: ae.Code} if len(ae.Params) > 0 { params := map[string]any(ae.Params) p.Params = ¶ms } return p } // GetUploadSettings реализует gen.StrictServerInterface. func (h *Handler) GetUploadSettings(ctx context.Context, _ gen.GetUploadSettingsRequestObject) (gen.GetUploadSettingsResponseObject, error) { s, err := h.svc.GetSettings(ctx) if err != nil { return nil, err } return gen.GetUploadSettings200JSONResponse(settingsDTO(s)), nil } // UpdateUploadSettings реализует gen.StrictServerInterface. func (h *Handler) UpdateUploadSettings(ctx context.Context, req gen.UpdateUploadSettingsRequestObject) (gen.UpdateUploadSettingsResponseObject, error) { b := req.Body s, err := h.svc.UpdateSettings(ctx, SettingsInput{ 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 }