Each disk stores .jukebox/profile.json with copy parameters (dest
folder, overwrite mode, file select, reserve space, auto-copy) and
optional transcoding limits for the target player (codec, resolution,
bitrate, FPS, audio channels, output format).
On copy, video files are probed with ffprobe; if they exceed the
profile limits they are transcoded via ffmpeg (-threads 0 for full
CPU usage), otherwise copied as-is. Scale filter never upscales.
New: internal/disk/profile.go, internal/transcoder/{detect,transcoder}.go
API: GET/PUT /api/disks/profile?mount_path=
UI: disk profile panel in dashboard for known disks
Dockerfile: adds ffmpeg to the runtime image
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
2.3 KiB
Go
113 lines
2.3 KiB
Go
package task
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type Status string
|
|
|
|
const (
|
|
StatusQueued Status = "queued"
|
|
StatusRunning Status = "running"
|
|
StatusSuccess Status = "success"
|
|
StatusFailed Status = "failed"
|
|
StatusCanceled Status = "canceled"
|
|
)
|
|
|
|
const (
|
|
PhaseQueued = "queued"
|
|
PhasePreparing = "preparing"
|
|
PhaseReplacing = "replacing"
|
|
PhaseLoadingHistory = "loading_history"
|
|
PhaseScanning = "scanning"
|
|
PhaseTranscoding = "transcoding"
|
|
PhaseCopying = "copying"
|
|
)
|
|
|
|
type Task struct {
|
|
ID string `json:"id"`
|
|
DiskID string `json:"disk_id"`
|
|
Type string `json:"type"`
|
|
Status Status `json:"status"`
|
|
Phase string `json:"phase,omitempty"`
|
|
Progress int `json:"progress"`
|
|
Message string `json:"message"`
|
|
SpeedBPS int64 `json:"speed_bps"`
|
|
ETASec int `json:"eta_sec"`
|
|
Error string `json:"error"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
func (t *Task) IsTerminal() bool {
|
|
return t.Status == StatusSuccess || t.Status == StatusFailed || t.Status == StatusCanceled
|
|
}
|
|
|
|
type Store struct {
|
|
mu sync.RWMutex
|
|
tasks map[string]*Task
|
|
}
|
|
|
|
func NewStore() *Store {
|
|
return &Store{tasks: make(map[string]*Task)}
|
|
}
|
|
|
|
func (s *Store) Create(taskType, diskID string) *Task {
|
|
now := time.Now().UTC()
|
|
t := &Task{
|
|
ID: uuid.New().String(),
|
|
DiskID: diskID,
|
|
Type: taskType,
|
|
Status: StatusQueued,
|
|
Phase: PhaseQueued,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
s.mu.Lock()
|
|
s.tasks[t.ID] = t
|
|
s.mu.Unlock()
|
|
return t
|
|
}
|
|
|
|
func (s *Store) Upsert(t Task) {
|
|
copy := t
|
|
s.mu.Lock()
|
|
s.tasks[t.ID] = ©
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *Store) Get(id string) (*Task, bool) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
t, ok := s.tasks[id]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
copy := *t
|
|
return ©, true
|
|
}
|
|
|
|
func (s *Store) Update(id string, fn func(*Task)) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if t, ok := s.tasks[id]; ok {
|
|
fn(t)
|
|
t.UpdatedAt = time.Now().UTC()
|
|
}
|
|
}
|
|
|
|
func (s *Store) ActiveTaskByDisk(diskID string) (*Task, bool) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
for _, t := range s.tasks {
|
|
if t.DiskID == diskID && (t.Status == StatusQueued || t.Status == StatusRunning) {
|
|
copy := *t
|
|
return ©, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|