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:
@@ -19,6 +19,7 @@ import (
|
||||
"jukebox_maker/internal/db"
|
||||
"jukebox_maker/internal/disk"
|
||||
"jukebox_maker/internal/task"
|
||||
"jukebox_maker/internal/transcoder"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
@@ -31,6 +32,7 @@ type Options struct {
|
||||
ReserveFreeGB float64
|
||||
OverwriteMode config.OverwriteMode
|
||||
FileSelectMode config.FileSelectMode
|
||||
Transcode *disk.TranscodeProfile // nil = не транскодировать
|
||||
}
|
||||
|
||||
type Copier struct {
|
||||
@@ -324,8 +326,14 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database
|
||||
}
|
||||
|
||||
dstAbs := filepath.Join(destRoot, f.relPath)
|
||||
if err := copyFile(ctx, f.srcAbs, dstAbs); err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
var fileErr error
|
||||
if opts.Transcode != nil && isVideoFile(f.srcAbs) {
|
||||
fileErr = c.processVideo(ctx, taskID, database, opts.Transcode, f.srcAbs, dstAbs)
|
||||
} else {
|
||||
fileErr = copyFile(ctx, f.srcAbs, dstAbs)
|
||||
}
|
||||
if fileErr != nil {
|
||||
if errors.Is(fileErr, context.Canceled) {
|
||||
c.tasks.Update(taskID, func(t *task.Task) {
|
||||
t.Status = task.StatusCanceled
|
||||
t.Message = "Canceled"
|
||||
@@ -353,6 +361,59 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database
|
||||
setStatus(task.StatusSuccess, fmt.Sprintf("Done. Copied %d files.", copied), 100)
|
||||
}
|
||||
|
||||
// videoExtensions — расширения видеофайлов из встроенного справочника.
|
||||
var videoExtensions = func() map[string]struct{} {
|
||||
exts := config.BuiltInMediaTypeExtensions()[config.MediaTypeVideo]
|
||||
set := make(map[string]struct{}, len(exts))
|
||||
for _, e := range exts {
|
||||
set[e] = struct{}{}
|
||||
}
|
||||
return set
|
||||
}()
|
||||
|
||||
func isVideoFile(path string) bool {
|
||||
_, ok := videoExtensions[strings.ToLower(filepath.Ext(path))]
|
||||
return ok
|
||||
}
|
||||
|
||||
// processVideo определяет: транскодировать или скопировать файл.
|
||||
func (c *Copier) processVideo(ctx context.Context, taskID string, database *db.DB, profile *disk.TranscodeProfile, src, dst string) error {
|
||||
info, err := transcoder.ProbeVideo(src)
|
||||
if err != nil {
|
||||
// Не смогли зондировать — просто копируем
|
||||
return copyFile(ctx, src, dst)
|
||||
}
|
||||
|
||||
if !transcoder.NeedsTranscode(info, profile) {
|
||||
return copyFile(ctx, src, dst)
|
||||
}
|
||||
|
||||
// Меняем расширение выходного файла под формат контейнера
|
||||
ext := transcoder.OutputExt(profile.OutputFormat)
|
||||
dstTranscoded := strings.TrimSuffix(dst, filepath.Ext(dst)) + ext
|
||||
|
||||
c.tasks.Update(taskID, func(t *task.Task) {
|
||||
t.Phase = task.PhaseTranscoding
|
||||
t.Message = "Transcoding " + filepath.Base(src)
|
||||
})
|
||||
if t, ok := c.tasks.Get(taskID); ok {
|
||||
_ = database.UpdateTask(*t)
|
||||
}
|
||||
|
||||
progressFn := func(pct float64) {
|
||||
c.tasks.Update(taskID, func(t *task.Task) {
|
||||
t.Progress = int(pct * 100)
|
||||
})
|
||||
}
|
||||
|
||||
return transcoder.Transcode(ctx, transcoder.Options{
|
||||
Input: src,
|
||||
Output: dstTranscoded,
|
||||
Profile: profile,
|
||||
SourceInfo: info,
|
||||
}, progressFn)
|
||||
}
|
||||
|
||||
type fileEntry struct {
|
||||
srcAbs string
|
||||
relPath string // relative to /media
|
||||
|
||||
Reference in New Issue
Block a user