Files
silo-server/internal/downloads/model.go
T
QuickandGitHub 47e45f4d67 feat(downloads): relay node-local artifacts through proxies (#608)
* feat(downloads): relay node-local artifacts through proxies

* fix(downloads): harden remote artifact relay

* fix(downloads): close distributed artifact review gaps

* fix(downloads): bound relays and refresh origins

* fix(downloads): recover stalled remote artifacts

* fix(transcode): report session cleanup failures

* fix(downloads): harden artifact cleanup recovery

* fix(downloads): close remote artifact lifecycle races

* fix(downloads): honor configured artifact storage

* fix(downloads): recover proxy-observed artifact misses

* test(downloads): isolate remote cleanup assertions

* fix(downloads): bound remote artifact recovery

* fix(downloads): allow concurrent artifact relays

* fix(downloads): harden distributed artifact recovery
2026-08-12 15:14:08 -04:00

149 lines
6.3 KiB
Go

// Package downloads owns the downloads domain: a single device-aware,
// quality-aware registry shared by the web app and mobile clients for offline
// playback. It replaces the former internal/download package, absorbing its
// bandwidth/quota/serving logic and reshaping the table and /downloads contract. See
// docs/superpowers/specs/2026-06-18-offline-sync-mobile-design.md.
package downloads
import (
"errors"
"time"
)
// Format constants are internal delivery formats recorded on a download row.
// They are not user-facing quality presets: clients choose a Quality*, then the
// service records the concrete delivery format it can fulfill.
const (
FormatOriginal = "original"
FormatRemux = "remux"
FormatTranscode = "transcode"
)
// Quality constants are the public download choices clients may request.
const (
QualityOriginal = "original"
Quality20Mbps = "20mbps"
Quality10Mbps = "10mbps"
Quality5Mbps = "5mbps"
Quality2Mbps = "2mbps"
Quality1Mbps = "1mbps"
)
// QualityPresets is the ordered UI/API ladder for download quality.
var QualityPresets = []string{
QualityOriginal,
Quality20Mbps,
Quality10Mbps,
Quality5Mbps,
Quality2Mbps,
Quality1Mbps,
}
// Download status constants.
const (
// Ephemeral / web lifecycle (unchanged from downloads v1):
// queued -> downloading -> completed | failed | canceled.
StatusQueued = "queued"
StatusDownloading = "downloading"
StatusCompleted = "completed"
StatusFailed = "failed"
StatusCancelled = "cancelled" //nolint:misspell // persisted DB enum value (migration 042)
// Managed device-entry lifecycle (Phase 1+):
// [preparing ->] ready -> completed, plus revoked (reserved for the
// admin revoke flow; see the download limits & restrictions design).
StatusPreparing = "preparing"
StatusReady = "ready"
StatusRevoked = "revoked"
)
// Download kind constants.
const (
KindQueued = "queued"
)
// Sentinel errors.
var (
ErrNotFound = errors.New("download not found")
ErrDownloadNotAllowed = errors.New("user is not allowed to download")
ErrFeatureDisabled = errors.New("downloads are disabled")
ErrConcurrentLimitReached = errors.New("concurrent download limit reached")
ErrPeriodLimitReached = errors.New("download period limit reached")
ErrDownloadNotActive = errors.New("download is not in an active state")
ErrStatusConflict = errors.New("download status transition conflict")
ErrTranscodeDisabled = errors.New("download transcode is disabled")
ErrInvalidQuality = errors.New("invalid download quality")
ErrProfileRequired = errors.New("managed download requires a profile")
ErrInvalidStatus = errors.New("invalid download status transition")
ErrManifestUnavailable = errors.New("offline manifest is not available")
ErrInvalidSubtitleRef = errors.New("invalid subtitle reference")
ErrAssetNotFound = errors.New("download asset not found")
ErrFormatUnavailable = errors.New("requested download format is not available")
// ErrResponseCommitted reports a transfer failure after response headers
// were written. Handlers must not append an API error body, while service
// lifecycle code must still treat the transfer as incomplete.
ErrResponseCommitted = errors.New("download response already committed")
// ErrArtifactOriginRemoved means a durable remote locator points at a node
// no longer present in the enabled transcode pool.
ErrArtifactOriginRemoved = errors.New("download artifact origin node was removed")
// ErrQualityUnavailable means the requested quality is valid and permitted in
// principle but cannot be fulfilled by the current server wiring.
ErrQualityUnavailable = errors.New("requested download quality is not available")
// ErrBulkQualityUnavailable keeps season/series batches original-only until
// batch artifact UX and storage reporting are explicit.
ErrBulkQualityUnavailable = errors.New("bulk quality downloads are not available")
// ErrNoDownloadableEpisodes means a series/season download matched a valid
// series but none of its episodes have a downloadable file.
ErrNoDownloadableEpisodes = errors.New("no downloadable episodes found")
// Series-monitoring (download subscription) errors.
ErrSubscriptionNotFound = errors.New("download subscription not found")
ErrSubscriptionsUnavailable = errors.New("download subscriptions are not available")
ErrInvalidSubscriptionMode = errors.New("invalid subscription mode")
ErrSeasonsRequired = errors.New("season_numbers is required for specific_seasons mode")
ErrInvalidSeasonNumbers = errors.New("season_numbers contains an out-of-range season number")
ErrNotSeries = errors.New("content_id is not a series")
)
// Download represents a row in the downloads table. It carries both the
// ephemeral/web lifecycle (DeviceID == "") and the managed device-entry
// lifecycle (DeviceID set); ProfileID/DeviceID/ArtifactID map to nullable
// columns and are empty strings when unset.
type Download struct {
ID string
UserID int
ProfileID string // "" for ephemeral/web rows
DeviceID string // "" = ephemeral; set = managed device entry
MediaFileID int
ContentID string
EpisodeID string
BatchID string
Kind string // direct or queued
Status string // see Status* constants
Format string // internal delivery format; see Format* constants
Quality string // user-requested quality; see Quality* constants
EffectiveQuality string // actual quality after original compatibility fallback
TargetBitrateKbps int // 0 for original/remux; bitrate cap for transcode
Revision int // increments when a managed row is replaced in place
ArtifactID string // "" until a remux/transcode artifact is linked (Phase 3)
FileSize int64
BytesSent int64
ErrorMessage string
CreatedAt time.Time
UpdatedAt time.Time
CompletedAt *time.Time
}
// SkippedDownload explains why a bulk series/season request did not create a
// row for an episode.
type SkippedDownload struct {
EpisodeID string `json:"episode_id"`
Reason string `json:"reason"`
}
// IsManaged reports whether this is a managed device-library entry (DeviceID set)
// rather than an ephemeral/account-level row.
func (d *Download) IsManaged() bool {
return d.DeviceID != ""
}