// Package uploads — очередь на добавление: резерв квоты, tus-приём файлов, обработка (ffprobe, хэши, Chromaprint, // проверка дублей), рассмотрение админом и импорт в хранилище через storage-agent (docs/uploads.md). package uploads import ( "strconv" "time" "github.com/google/uuid" "pipu/api/internal/library/meta" ) // Status — состояние файла в очереди (docs/uploads.md#2-жизненный-цикл-загрузки). type Status string // Статусы загрузки. const ( StatusUploading Status = "uploading" StatusProcessing Status = "processing" StatusPendingReview Status = "pending_review" StatusApproved Status = "approved" StatusImporting Status = "importing" StatusImported Status = "imported" StatusImportFailed Status = "import_failed" StatusDuplicate Status = "duplicate" StatusInvalid Status = "invalid" StatusRejected Status = "rejected" StatusWithdrawn Status = "withdrawn" StatusExpired Status = "expired" ) // Occupies — занимает ли файл в этом статусе место в очереди (docs/uploads.md#2, таблица). func (s Status) Occupies() bool { switch s { case StatusUploading, StatusProcessing, StatusPendingReview, StatusApproved, StatusImporting, StatusImportFailed: return true } return false } // KeepsStaging — файл в этом статусе ещё нужен в staging. func (s Status) KeepsStaging() bool { return s.Occupies() } // Коды ошибок и причин. const ( CodeQueueFull = "uploads.queue_full" CodeUserQuotaFull = "uploads.user_quota_full" CodeTooLarge = "uploads.too_large" CodeDuplicate = "uploads.duplicate" CodeInvalidAudio = "uploads.invalid_audio" CodeBadExtension = "uploads.unsupported_format" CodeNotFound = "uploads.not_found" CodeWrongStatus = "uploads.wrong_status" CodeNoTarget = "uploads.no_target" CodeOffset = "uploads.offset_mismatch" CodeSizeMismatch = "uploads.size_mismatch" CodeHashMismatch = "uploads.hash_mismatch" CodeMissingField = "uploads.missing_field" CodeImportFailed = "uploads.import_failed" CodeTemplate = "uploads.no_template" ) // Уровни дублей. const ( DupSHA256 = "sha256" DupMetadata = "metadata" DupFingerprint = "fingerprint" ) // Meta — итоговые поля трека загрузки; их видит и правит админ, по ним строится путь в хранилище. type Meta struct { Artist string `json:"artist"` AlbumArtist string `json:"albumartist"` Album string `json:"album"` Title string `json:"title"` Genre string `json:"genre"` Year *int `json:"year,omitempty"` Track *int `json:"track,omitempty"` Disc *int `json:"disc,omitempty"` } // Fields — значения для шаблона пути. func (m Meta) Fields() meta.Fields { num := func(p *int) string { if p == nil || *p <= 0 { return "" } return strconv.Itoa(*p) } return meta.Fields{ Artist: m.Artist, AlbumArtist: m.AlbumArtist, Album: m.Album, Title: m.Title, Genre: m.Genre, Year: num(m.Year), Track: num(m.Track), Disc: num(m.Disc), } } // FromMerged — Meta из результата слияния тегов и пути. func FromMerged(m meta.Meta) Meta { ptr := func(v int) *int { if v <= 0 { return nil } return &v } out := Meta{Artist: m.Artist, AlbumArtist: m.AlbumArtist, Album: m.Album, Title: m.Title, Year: ptr(m.Year), Track: ptr(m.Track), Disc: ptr(m.Disc)} if len(m.Genres) > 0 { out.Genre = m.Genres[0] } return out } // File — файл очереди. type File struct { ID uuid.UUID BatchID uuid.UUID AuthorID uuid.UUID AuthorName string BatchComment *string Status Status OriginalName string ClientRelPath *string Size int64 Received int64 SHA256 []byte StagingPath string Format *string Codec *string Bitrate *int SampleRate *int BitDepth *int DurationMs *int Tags map[string]string Meta *Meta MetadataEdited bool MatchedArtistID *uuid.UUID MatchedAlbumID *uuid.UUID DuplicateOfTrackID *uuid.UUID DuplicateLevel *string PossibleDuplicate bool QualityUpgrade bool TargetSourceID *uuid.UUID TargetSourceName *string TargetStoragePath *string TargetRelPath *string StatusReason *string ReviewedBy *uuid.UUID ReviewedAt *time.Time ImportedFileID *uuid.UUID CoverPath *string CreatedAt time.Time UpdatedAt time.Time } // Settings — лимиты очереди и хранилище по умолчанию (app_settings, меняет админ). type Settings struct { QueueLimitBytes int64 `json:"queue_limit_bytes"` PerUserLimitBytes *int64 `json:"per_user_limit_bytes,omitempty"` // nil — выключен MaxFileBytes int64 `json:"max_file_bytes"` DefaultSourceID *uuid.UUID `json:"default_source_id,omitempty"` AllowQualityUpgrade bool `json:"allow_quality_upgrade"` } // DefaultSettings — значения по умолчанию (docs/uploads.md#3-квота-очереди). var DefaultSettings = Settings{QueueLimitBytes: 10 << 30, MaxFileBytes: 2 << 30} // AllowedExtensions — форматы, которые принимает очередь (содержимое всё равно проверяет ffprobe). var AllowedExtensions = map[string]bool{ "flac": true, "mp3": true, "m4a": true, "ogg": true, "opus": true, "wav": true, "aiff": true, "aif": true, "wv": true, } // Пределы длительности (docs/uploads.md#5-загрузка). const ( MinDurationMs = 5_000 MaxDurationMs = 3 * 60 * 60 * 1000 ) // ExpireAfter — недокачанная загрузка истекает (docs/uploads.md#11-очистка). const ExpireAfter = 24 * time.Hour