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>
154 lines
4.2 KiB
Go
154 lines
4.2 KiB
Go
package api
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"time"
|
||
|
||
"jukebox_maker/internal/config"
|
||
"jukebox_maker/internal/disk"
|
||
)
|
||
|
||
func (s *Server) diskResponse(info disk.DiskInfo) map[string]any {
|
||
item := map[string]any{
|
||
"state": info.State,
|
||
"disk_id": info.DiskID,
|
||
"total_bytes": info.TotalBytes,
|
||
"free_bytes": info.FreeBytes,
|
||
"mount_path": info.MountPath,
|
||
"profile": info.Profile,
|
||
}
|
||
if info.DiskID != "" {
|
||
if s.deps.OnDiskInit != nil {
|
||
s.deps.OnDiskInit(info.MountPath, info.DiskID)
|
||
}
|
||
if lastCopiedAt, ok, err := s.deps.Copier.LastCopiedAt(info.DiskID); err == nil && ok {
|
||
item["last_copied_at"] = lastCopiedAt.Format(time.RFC3339)
|
||
}
|
||
if t, ok := s.deps.Tasks.ActiveTaskByDisk(info.DiskID); ok {
|
||
item["active_task_id"] = t.ID
|
||
}
|
||
}
|
||
return item
|
||
}
|
||
|
||
func (s *Server) handleDiskStatus(w http.ResponseWriter, r *http.Request) {
|
||
type response struct {
|
||
State disk.DiskState `json:"state"`
|
||
DiskID string `json:"disk_id"`
|
||
TotalBytes int64 `json:"total_bytes"`
|
||
FreeBytes int64 `json:"free_bytes"`
|
||
MountPath string `json:"mount_path"`
|
||
LastCopiedAt string `json:"last_copied_at,omitempty"`
|
||
ActiveTaskID string `json:"active_task_id,omitempty"`
|
||
}
|
||
|
||
disks := s.deps.Watcher.ListDisks()
|
||
resp := make([]response, 0, len(disks))
|
||
for _, info := range disks {
|
||
item := response{
|
||
State: info.State,
|
||
DiskID: info.DiskID,
|
||
TotalBytes: info.TotalBytes,
|
||
FreeBytes: info.FreeBytes,
|
||
MountPath: info.MountPath,
|
||
}
|
||
if payload := s.diskResponse(info); payload != nil {
|
||
if v, ok := payload["last_copied_at"].(string); ok {
|
||
item.LastCopiedAt = v
|
||
}
|
||
if v, ok := payload["active_task_id"].(string); ok {
|
||
item.ActiveTaskID = v
|
||
}
|
||
}
|
||
resp = append(resp, item)
|
||
}
|
||
|
||
jsonOK(w, map[string]any{"items": resp})
|
||
}
|
||
|
||
func (s *Server) handleDiskProbe(w http.ResponseWriter, r *http.Request) {
|
||
mountPath := r.URL.Query().Get("mount_path")
|
||
info, err := s.deps.ProbeDisk(mountPath)
|
||
if err != nil {
|
||
jsonErr(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
jsonOK(w, s.diskResponse(info))
|
||
}
|
||
|
||
func (s *Server) handleDiskInit(w http.ResponseWriter, r *http.Request) {
|
||
var req struct {
|
||
MountPath string `json:"mount_path"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
jsonErr(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||
return
|
||
}
|
||
|
||
info, err := s.deps.ProbeDisk(req.MountPath)
|
||
if err != nil {
|
||
jsonErr(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
if info.State == disk.DiskAbsent {
|
||
jsonErr(w, http.StatusUnprocessableEntity, "no disk connected")
|
||
return
|
||
}
|
||
if info.State == disk.DiskKnown {
|
||
jsonErr(w, http.StatusConflict, "disk already initialized")
|
||
return
|
||
}
|
||
if err := disk.CheckWritable(info.MountPath); err != nil {
|
||
jsonErr(w, http.StatusUnprocessableEntity, "disk is not writable: "+err.Error())
|
||
return
|
||
}
|
||
|
||
diskID, err := disk.InitDisk(info.MountPath)
|
||
if err != nil {
|
||
jsonErr(w, http.StatusInternalServerError, "init disk: "+err.Error())
|
||
return
|
||
}
|
||
|
||
if s.deps.OnDiskInit != nil {
|
||
s.deps.OnDiskInit(info.MountPath, diskID)
|
||
}
|
||
jsonOK(w, map[string]string{"disk_id": diskID})
|
||
}
|
||
|
||
func (s *Server) handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||
mountPath := config.NormalizeMediaPath(r.URL.Query().Get("mount_path"))
|
||
if mountPath == "" {
|
||
jsonErr(w, http.StatusBadRequest, "mount_path is required")
|
||
return
|
||
}
|
||
p, err := disk.LoadProfile(mountPath)
|
||
if err != nil {
|
||
jsonErr(w, http.StatusNotFound, "profile not found")
|
||
return
|
||
}
|
||
jsonOK(w, p)
|
||
}
|
||
|
||
func (s *Server) handlePutProfile(w http.ResponseWriter, r *http.Request) {
|
||
mountPath := config.NormalizeMediaPath(r.URL.Query().Get("mount_path"))
|
||
if mountPath == "" {
|
||
jsonErr(w, http.StatusBadRequest, "mount_path is required")
|
||
return
|
||
}
|
||
var p disk.DiskProfile
|
||
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
||
jsonErr(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||
return
|
||
}
|
||
if err := disk.SaveProfile(mountPath, &p); err != nil {
|
||
jsonErr(w, http.StatusInternalServerError, "save profile: "+err.Error())
|
||
return
|
||
}
|
||
// Обновляем информацию о диске в watcher если он там есть
|
||
if s.deps.Watcher != nil {
|
||
s.deps.Watcher.ProbeNow()
|
||
}
|
||
jsonOK(w, &p)
|
||
}
|