Files
pipu-music/services/api/internal/uploads/handler.go
T
mrleo1nid 82bf015283
ci / module boundaries / boundaries (push) Successful in 6s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 2m1s
ci / ts / ts (packages + web) (push) Failing after 5m13s
ci / api / api (push) Successful in 41s
ci / db / db (push) Failing after 1m33s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 2m25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 2m19s
Import missing artist portraits from uploaded album folders
2026-10-30 10:46:07 +03:00

361 lines
12 KiB
Go

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
}
}
var artistImage []byte
if req.Body.ArtistImageBase64 != nil {
var err error
artistImage, err = DecodeCover(*req.Body.ArtistImageBase64)
if err != nil {
return nil, err
}
}
batch, res, err := h.svc.Create(ctx, req.Body.Comment, in, cover, artistImage)
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 = &params
}
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
}