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
181 lines
6.1 KiB
Go
181 lines
6.1 KiB
Go
// 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
|
|
ArtistImagePath *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
|