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 {
|
} else {
|
||||||
d.thumbTokenBucket = NewStaticTokenBucketWithMigration(d.thumbTokenBucket, d.thumbConcurrency)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ type Addition struct {
|
||||||
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
|
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
|
||||||
ThumbCacheFolder string `json:"thumb_cache_folder"`
|
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."`
|
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"`
|
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
|
||||||
MkdirPerm string `json:"mkdir_perm" default:"777"`
|
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'"`
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
|
@ -34,10 +36,58 @@ func isSymlinkDir(f fs.FileInfo, path string) bool {
|
||||||
return false
|
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)
|
srcBuf := bytes.NewBuffer(nil)
|
||||||
stream := ffmpeg.Input(videoPath).
|
// If the remaining time from the seek point to the end of the video is less
|
||||||
Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}).
|
// 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"}).
|
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
|
||||||
GlobalArgs("-loglevel", "error").Silent(true).
|
GlobalArgs("-loglevel", "error").Silent(true).
|
||||||
WithOutput(srcBuf, os.Stdout)
|
WithOutput(srcBuf, os.Stdout)
|
||||||
|
@ -77,7 +127,7 @@ func (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {
|
||||||
}
|
}
|
||||||
var srcBuf *bytes.Buffer
|
var srcBuf *bytes.Buffer
|
||||||
if utils.GetFileType(file.GetName()) == conf.VIDEO {
|
if utils.GetFileType(file.GetName()) == conf.VIDEO {
|
||||||
videoBuf, err := GetSnapshot(fullPath, 10)
|
videoBuf, err := d.GetSnapshot(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue