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 thumbnail
pull/7716/head^2
Lin Tianchuan 2025-01-10 20:48:45 +08:00 committed by GitHub
parent 687124c81d
commit 31a7470865
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 77 additions and 4 deletions

View File

@ -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
} }

View File

@ -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'"`

View File

@ -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
} }