Files
jukebox_maker/internal/transcoder/detect.go
Michael Chus 9fd02fb5bf 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>
2026-05-21 20:52:46 +03:00

173 lines
3.7 KiB
Go

package transcoder
import (
"encoding/json"
"os/exec"
"strconv"
"strings"
"jukebox_maker/internal/disk"
)
type VideoInfo struct {
Codec string
Width int
Height int
FPS float64
DurationSec float64
VideoBitrate int64
AudioCodec string
AudioChannels int
}
func ProbeVideo(path string) (VideoInfo, error) {
out, err := exec.Command("ffprobe",
"-v", "quiet",
"-print_format", "json",
"-show_streams",
"-show_format",
path,
).Output()
if err != nil {
return VideoInfo{}, err
}
var raw struct {
Streams []struct {
CodecType string `json:"codec_type"`
CodecName string `json:"codec_name"`
Width int `json:"width"`
Height int `json:"height"`
RFrameRate string `json:"r_frame_rate"`
BitRate string `json:"bit_rate"`
Channels int `json:"channels"`
} `json:"streams"`
Format struct {
Duration string `json:"duration"`
BitRate string `json:"bit_rate"`
} `json:"format"`
}
if err := json.Unmarshal(out, &raw); err != nil {
return VideoInfo{}, err
}
var info VideoInfo
for _, s := range raw.Streams {
switch s.CodecType {
case "video":
info.Codec = s.CodecName
info.Width = s.Width
info.Height = s.Height
info.FPS = parseFraction(s.RFrameRate)
if br, err := strconv.ParseInt(s.BitRate, 10, 64); err == nil {
info.VideoBitrate = br
}
case "audio":
if info.AudioCodec == "" {
info.AudioCodec = s.CodecName
info.AudioChannels = s.Channels
}
}
}
if d, err := strconv.ParseFloat(raw.Format.Duration, 64); err == nil {
info.DurationSec = d
}
// Если стрим-битрейт не известен — используем битрейт контейнера
if info.VideoBitrate == 0 {
if br, err := strconv.ParseInt(raw.Format.BitRate, 10, 64); err == nil {
info.VideoBitrate = br
}
}
return info, nil
}
func NeedsTranscode(info VideoInfo, p *disk.TranscodeProfile) bool {
// Несовместимый видеокодек
if normalizeCodec(info.Codec) != p.VideoCodec {
return true
}
// Разрешение превышает лимит
if maxH := maxHeight(p.MaxResolution); maxH > 0 && info.Height > maxH {
return true
}
// Битрейт видео превышает лимит
if p.MaxVideoBitrate != "" {
if maxBR := parseBitrate(p.MaxVideoBitrate); maxBR > 0 && info.VideoBitrate > maxBR {
return true
}
}
// FPS превышает лимит
if p.MaxFPS > 0 && info.FPS > float64(p.MaxFPS)+0.01 {
return true
}
// Несовместимый аудиокодек
if p.AudioCodec != "" && normalizeCodec(info.AudioCodec) != p.AudioCodec {
return true
}
// Каналов больше лимита
if p.MaxAudioChannels > 0 && info.AudioChannels > p.MaxAudioChannels {
return true
}
return false
}
func normalizeCodec(codec string) string {
switch strings.ToLower(codec) {
case "hevc", "h265":
return "h265"
case "h264", "avc", "avc1":
return "h264"
case "mpeg4", "mp4v":
return "mpeg4"
case "aac":
return "aac"
case "mp3":
return "mp3"
default:
return strings.ToLower(codec)
}
}
func maxHeight(res string) int {
switch res {
case "480p":
return 480
case "720p":
return 720
case "1080p":
return 1080
default:
return 0
}
}
func parseBitrate(s string) int64 {
s = strings.ToLower(strings.TrimSpace(s))
if strings.HasSuffix(s, "k") {
v, _ := strconv.ParseInt(s[:len(s)-1], 10, 64)
return v * 1000
}
v, _ := strconv.ParseInt(s, 10, 64)
return v
}
func parseFraction(s string) float64 {
parts := strings.SplitN(s, "/", 2)
if len(parts) != 2 {
v, _ := strconv.ParseFloat(s, 64)
return v
}
num, _ := strconv.ParseFloat(parts[0], 64)
den, _ := strconv.ParseFloat(parts[1], 64)
if den == 0 {
return 0
}
return num / den
}