mirror of https://github.com/Xhofe/alist
feat(local): support both time and percent for video thumbnail (#7802)
* feat(local): support percent for video thumbnail The percentage determines the point in the video (as a percentage of the total duration) at which the thumbnail will be generated. * feat(local): support both time and percent for video thumbnailpull/7716/head^2
parent
687124c81d
commit
31a7470865
|
@ -79,6 +79,28 @@ func (d *Local) Init(ctx context.Context) error {
|
|||
} else {
|
||||
d.thumbTokenBucket = NewStaticTokenBucketWithMigration(d.thumbTokenBucket, d.thumbConcurrency)
|
||||
}
|
||||
// Check the VideoThumbPos value
|
||||
if d.VideoThumbPos == "" {
|
||||
d.VideoThumbPos = "20%"
|
||||
}
|
||||
if strings.HasSuffix(d.VideoThumbPos, "%") {
|
||||
percentage := strings.TrimSuffix(d.VideoThumbPos, "%")
|
||||
val, err := strconv.ParseFloat(percentage, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid video_thumb_pos value: %s, err: %s", d.VideoThumbPos, err)
|
||||
}
|
||||
if val < 0 || val > 100 {
|
||||
return fmt.Errorf("invalid video_thumb_pos value: %s, the precentage must be a number between 0 and 100", d.VideoThumbPos)
|
||||
}
|
||||
} else {
|
||||
val, err := strconv.ParseFloat(d.VideoThumbPos, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid video_thumb_pos value: %s, err: %s", d.VideoThumbPos, err)
|
||||
}
|
||||
if val < 0 {
|
||||
return fmt.Errorf("invalid video_thumb_pos value: %s, the time must be a positive number", d.VideoThumbPos)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ type Addition struct {
|
|||
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
|
||||
ThumbCacheFolder string `json:"thumb_cache_folder"`
|
||||
ThumbConcurrency string `json:"thumb_concurrency" default:"16" required:"false" help:"Number of concurrent thumbnail generation goroutines. This controls how many thumbnails can be generated in parallel."`
|
||||
VideoThumbPos string `json:"video_thumb_pos" default:"20%" required:"false" help:"The position of the video thumbnail. If the value is a number (integer ot floating point), it represents the time in seconds. If the value ends with '%', it represents the percentage of the video duration."`
|
||||
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
|
||||
MkdirPerm string `json:"mkdir_perm" default:"777"`
|
||||
RecycleBinPath string `json:"recycle_bin_path" default:"delete permanently" help:"path to recycle bin, delete permanently if empty or keep 'delete permanently'"`
|
||||
|
|
|
@ -2,11 +2,13 @@ package local
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
|
@ -34,10 +36,58 @@ func isSymlinkDir(f fs.FileInfo, path string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func GetSnapshot(videoPath string, frameNum int) (imgData *bytes.Buffer, err error) {
|
||||
// Get the snapshot of the video
|
||||
func (d *Local) GetSnapshot(videoPath string) (imgData *bytes.Buffer, err error) {
|
||||
// Run ffprobe to get the video duration
|
||||
jsonOutput, err := ffmpeg.Probe(videoPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// get format.duration from the json string
|
||||
type probeFormat struct {
|
||||
Duration string `json:"duration"`
|
||||
}
|
||||
type probeData struct {
|
||||
Format probeFormat `json:"format"`
|
||||
}
|
||||
var probe probeData
|
||||
err = json.Unmarshal([]byte(jsonOutput), &probe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalDuration, err := strconv.ParseFloat(probe.Format.Duration, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ss string
|
||||
if strings.HasSuffix(d.VideoThumbPos, "%") {
|
||||
percentage, err := strconv.ParseFloat(strings.TrimSuffix(d.VideoThumbPos, "%"), 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ss = fmt.Sprintf("%f", totalDuration*percentage/100)
|
||||
} else {
|
||||
val, err := strconv.ParseFloat(d.VideoThumbPos, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If the value is greater than the total duration, use the total duration
|
||||
if val > totalDuration {
|
||||
ss = fmt.Sprintf("%f", totalDuration)
|
||||
} else {
|
||||
ss = d.VideoThumbPos
|
||||
}
|
||||
}
|
||||
|
||||
// Run ffmpeg to get the snapshot
|
||||
srcBuf := bytes.NewBuffer(nil)
|
||||
stream := ffmpeg.Input(videoPath).
|
||||
Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}).
|
||||
// If the remaining time from the seek point to the end of the video is less
|
||||
// than the duration of a single frame, ffmpeg cannot extract any frames
|
||||
// within the specified range and will exit with an error.
|
||||
// The "noaccurate_seek" option prevents this error and would also speed up
|
||||
// the seek process.
|
||||
stream := ffmpeg.Input(videoPath, ffmpeg.KwArgs{"ss": ss, "noaccurate_seek": ""}).
|
||||
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
|
||||
GlobalArgs("-loglevel", "error").Silent(true).
|
||||
WithOutput(srcBuf, os.Stdout)
|
||||
|
@ -77,7 +127,7 @@ func (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {
|
|||
}
|
||||
var srcBuf *bytes.Buffer
|
||||
if utils.GetFileType(file.GetName()) == conf.VIDEO {
|
||||
videoBuf, err := GetSnapshot(fullPath, 10)
|
||||
videoBuf, err := d.GetSnapshot(fullPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue