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>
95 lines
2.0 KiB
Go
95 lines
2.0 KiB
Go
package disk
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type DiskState string
|
|
|
|
const (
|
|
DiskAbsent DiskState = "absent"
|
|
DiskForeign DiskState = "foreign"
|
|
DiskKnown DiskState = "known"
|
|
)
|
|
|
|
type DiskInfo struct {
|
|
State DiskState `json:"state"`
|
|
DiskID string `json:"disk_id"`
|
|
TotalBytes int64 `json:"total_bytes"`
|
|
FreeBytes int64 `json:"free_bytes"`
|
|
MountPath string `json:"mount_path"`
|
|
Profile *DiskProfile `json:"profile,omitempty"`
|
|
}
|
|
|
|
const MarkerDir = ".jukebox"
|
|
const idFile = "disk.id"
|
|
|
|
func Probe(mountPath string) (DiskInfo, error) {
|
|
info := DiskInfo{MountPath: mountPath, State: DiskAbsent}
|
|
|
|
if _, err := os.ReadDir(mountPath); err != nil {
|
|
return info, nil
|
|
}
|
|
|
|
total, free, err := DiskUsage(mountPath)
|
|
if err != nil {
|
|
return info, nil
|
|
}
|
|
info.TotalBytes = total
|
|
info.FreeBytes = free
|
|
|
|
idPath := filepath.Join(mountPath, MarkerDir, idFile)
|
|
data, err := os.ReadFile(idPath)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
info.State = DiskForeign
|
|
return info, nil
|
|
}
|
|
if err != nil {
|
|
info.State = DiskForeign
|
|
return info, nil
|
|
}
|
|
|
|
info.DiskID = strings.TrimSpace(string(data))
|
|
info.State = DiskKnown
|
|
if p, err := LoadProfile(mountPath); err == nil {
|
|
info.Profile = p
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func CheckWritable(path string) error {
|
|
f, err := os.CreateTemp(path, ".jukebox-writecheck-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := f.Name()
|
|
if err := f.Close(); err != nil {
|
|
_ = os.Remove(name)
|
|
return err
|
|
}
|
|
return os.Remove(name)
|
|
}
|
|
|
|
func InitDisk(mountPath string) (string, error) {
|
|
dir := filepath.Join(mountPath, MarkerDir)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
id := uuid.New().String()
|
|
idPath := filepath.Join(dir, idFile)
|
|
if err := os.WriteFile(idPath, []byte(id), 0o644); err != nil {
|
|
return "", err
|
|
}
|
|
_ = SaveProfile(mountPath, DefaultProfile())
|
|
return id, nil
|
|
}
|
|
|
|
func DBPath(mountPath string) string {
|
|
return filepath.Join(mountPath, MarkerDir, "history.db")
|
|
}
|