Revert to Docker-only source paths, fix config validation, improve transcoding info
- handlers_sources.go: revert to relative paths rooted at /media (remove standalone absolute-path mode) - settings.html: remove manual path input, restore auto-loading source tree from /media - config.go: remove filesystem existence checks from Validate() — paths may be temporarily unavailable - transcoder.go: always specify fps in ffmpeg args when MaxFPS is set, preserving source fps if lower than limit - copier.go: include source codec/format and target codec/format in transcoding task message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -10,19 +11,20 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) handleSources(w http.ResponseWriter, r *http.Request) {
|
||||
absPath, err := normalizeSourcePathQuery(r.URL.Query().Get("path"))
|
||||
relPath, err := normalizeSourcePath(r.URL.Query().Get("path"))
|
||||
if err != nil {
|
||||
jsonErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if absPath == "" {
|
||||
jsonOK(w, map[string]any{"path": "", "items": []map[string]string{}})
|
||||
return
|
||||
|
||||
absPath := s.deps.Config.MediaPath
|
||||
if relPath != "" {
|
||||
absPath = filepath.Join(absPath, relPath)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(absPath)
|
||||
if err != nil {
|
||||
jsonOK(w, map[string]any{"path": absPath, "items": []map[string]string{}})
|
||||
jsonOK(w, map[string]any{"path": relPath, "items": []map[string]string{}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,10 +38,13 @@ func (s *Server) handleSources(w http.ResponseWriter, r *http.Request) {
|
||||
if !e.IsDir() || strings.HasPrefix(e.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
childPath := filepath.Join(absPath, e.Name())
|
||||
childPath := e.Name()
|
||||
if relPath != "" {
|
||||
childPath = filepath.Join(relPath, childPath)
|
||||
}
|
||||
items = append(items, item{
|
||||
Name: e.Name(),
|
||||
Path: childPath,
|
||||
Path: filepath.ToSlash(childPath),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,18 +53,26 @@ func (s *Server) handleSources(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
|
||||
jsonOK(w, map[string]any{
|
||||
"path": absPath,
|
||||
"path": relPath,
|
||||
"items": items,
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeSourcePathQuery(raw string) (string, error) {
|
||||
func normalizeSourcePath(raw string) (string, error) {
|
||||
raw, _ = url.QueryUnescape(raw)
|
||||
raw = strings.TrimSpace(raw)
|
||||
raw = filepath.ToSlash(raw)
|
||||
raw = strings.TrimPrefix(raw, "/")
|
||||
if raw == "" || raw == "." {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
clean := filepath.Clean(raw)
|
||||
if !filepath.IsAbs(clean) {
|
||||
clean = filepath.ToSlash(clean)
|
||||
if clean == "." {
|
||||
return "", nil
|
||||
}
|
||||
if clean == ".." || strings.HasPrefix(clean, "../") {
|
||||
return "", errors.New("invalid source path")
|
||||
}
|
||||
return clean, nil
|
||||
|
||||
@@ -116,25 +116,7 @@ func Save(path string, cfg *Config) error {
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
c.MediaPath = NormalizeMediaPath(c.MediaPath)
|
||||
if c.MediaPath != "" {
|
||||
info, err := os.Stat(c.MediaPath)
|
||||
if err != nil {
|
||||
return errors.New("media_path is not accessible")
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return errors.New("media_path must be a directory")
|
||||
}
|
||||
}
|
||||
c.Sources = NormalizeSources(c.Sources, c.MediaPath)
|
||||
for _, source := range c.Sources {
|
||||
info, err := os.Stat(source.Path)
|
||||
if err != nil {
|
||||
return errors.New("source path is not accessible: " + source.Path)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return errors.New("source path must be a directory: " + source.Path)
|
||||
}
|
||||
}
|
||||
if c.ReserveFreeGB < 0 {
|
||||
return errors.New("reserve_free_gb must be >= 0")
|
||||
}
|
||||
|
||||
@@ -392,9 +392,15 @@ func (c *Copier) processVideo(ctx context.Context, taskID string, database *db.D
|
||||
ext := transcoder.OutputExt(profile.OutputFormat)
|
||||
dstTranscoded := strings.TrimSuffix(dst, filepath.Ext(dst)) + ext
|
||||
|
||||
srcFPS := fmt.Sprintf("%.2f", info.FPS)
|
||||
msg := fmt.Sprintf("Transcoding %s (%s/%dch/%sfps → %s/%s/%dfps %s)",
|
||||
filepath.Base(src),
|
||||
info.Codec, info.AudioChannels, srcFPS,
|
||||
profile.VideoCodec, profile.AudioCodec, profile.MaxFPS, profile.OutputFormat,
|
||||
)
|
||||
c.tasks.Update(taskID, func(t *task.Task) {
|
||||
t.Phase = task.PhaseTranscoding
|
||||
t.Message = "Transcoding " + filepath.Base(src)
|
||||
t.Message = msg
|
||||
})
|
||||
if t, ok := c.tasks.Get(taskID); ok {
|
||||
_ = database.UpdateTask(*t)
|
||||
|
||||
@@ -97,8 +97,14 @@ func buildArgs(opts Options) []string {
|
||||
if maxH := maxHeight(p.MaxResolution); maxH > 0 {
|
||||
filters = append(filters, fmt.Sprintf("scale=-2:min(%d\\,ih)", maxH))
|
||||
}
|
||||
if p.MaxFPS > 0 && src.FPS > float64(p.MaxFPS)+0.01 {
|
||||
filters = append(filters, fmt.Sprintf("fps=%d", p.MaxFPS))
|
||||
if p.MaxFPS > 0 {
|
||||
targetFPS := p.MaxFPS
|
||||
if src.FPS > 0 && src.FPS < float64(targetFPS) {
|
||||
// Источник медленнее лимита — сохраняем исходный FPS
|
||||
filters = append(filters, fmt.Sprintf("fps=%.3f", src.FPS))
|
||||
} else {
|
||||
filters = append(filters, fmt.Sprintf("fps=%d", targetFPS))
|
||||
}
|
||||
}
|
||||
if len(filters) > 0 {
|
||||
args = append(args, "-vf", strings.Join(filters, ","))
|
||||
|
||||
Reference in New Issue
Block a user