Add per-disk profiles with video transcoding support
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>
This commit is contained in:
70
internal/disk/profile.go
Normal file
70
internal/disk/profile.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type DiskProfile struct {
|
||||
DestFolder string `json:"dest_folder"`
|
||||
OverwriteMode string `json:"overwrite_mode"`
|
||||
FileSelectMode string `json:"file_select_mode"`
|
||||
ReserveFreeGB float64 `json:"reserve_free_gb"`
|
||||
AutoCopy bool `json:"auto_copy"`
|
||||
|
||||
// nil = не транскодировать видео
|
||||
Transcode *TranscodeProfile `json:"transcode,omitempty"`
|
||||
}
|
||||
|
||||
type TranscodeProfile struct {
|
||||
VideoCodec string `json:"video_codec"` // "h264" | "h265" | "mpeg4"
|
||||
MaxResolution string `json:"max_resolution"` // "480p" | "720p" | "1080p"
|
||||
MaxVideoBitrate string `json:"max_video_bitrate"` // "1000k" | "2000k" | "" (без лимита)
|
||||
MaxFPS int `json:"max_fps"` // 0=без лимита | 24 | 25 | 30
|
||||
AudioCodec string `json:"audio_codec"` // "aac" | "mp3"
|
||||
MaxAudioBitrate string `json:"max_audio_bitrate"` // "128k" | "192k" | ""
|
||||
MaxAudioChannels int `json:"max_audio_channels"` // 2=стерео | 6=5.1
|
||||
OutputFormat string `json:"output_format"` // "mp4" | "mkv" | "avi"
|
||||
}
|
||||
|
||||
const profileFile = "profile.json"
|
||||
|
||||
func profilePath(mountPath string) string {
|
||||
return filepath.Join(mountPath, MarkerDir, profileFile)
|
||||
}
|
||||
|
||||
func LoadProfile(mountPath string) (*DiskProfile, error) {
|
||||
data, err := os.ReadFile(profilePath(mountPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var p DiskProfile
|
||||
if err := json.Unmarshal(data, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func SaveProfile(mountPath string, p *DiskProfile) error {
|
||||
data, err := json.MarshalIndent(p, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := profilePath(mountPath)
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
func DefaultProfile() *DiskProfile {
|
||||
return &DiskProfile{
|
||||
DestFolder: "media",
|
||||
OverwriteMode: "skip",
|
||||
FileSelectMode: "new",
|
||||
ReserveFreeGB: 2.0,
|
||||
AutoCopy: false,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user