2022-06-23 09:06:07 +00:00
|
|
|
package local
|
2022-11-09 03:20:09 +00:00
|
|
|
|
|
|
|
import (
|
2023-02-22 13:19:42 +00:00
|
|
|
"bytes"
|
2025-01-10 12:48:45 +00:00
|
|
|
"encoding/json"
|
2023-02-22 13:19:42 +00:00
|
|
|
"fmt"
|
2022-11-09 03:20:09 +00:00
|
|
|
"io/fs"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2023-03-23 07:18:37 +00:00
|
|
|
"sort"
|
2025-01-10 12:48:45 +00:00
|
|
|
"strconv"
|
2023-05-11 11:57:24 +00:00
|
|
|
"strings"
|
2023-03-23 07:18:37 +00:00
|
|
|
|
2023-05-11 11:57:24 +00:00
|
|
|
"github.com/alist-org/alist/v3/internal/conf"
|
|
|
|
"github.com/alist-org/alist/v3/internal/model"
|
|
|
|
"github.com/alist-org/alist/v3/pkg/utils"
|
|
|
|
"github.com/disintegration/imaging"
|
2023-03-23 07:18:37 +00:00
|
|
|
ffmpeg "github.com/u2takey/ffmpeg-go"
|
2022-11-09 03:20:09 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func isSymlinkDir(f fs.FileInfo, path string) bool {
|
|
|
|
if f.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
|
|
dst, err := os.Readlink(filepath.Join(path, f.Name()))
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
2022-11-09 10:15:42 +00:00
|
|
|
if !filepath.IsAbs(dst) {
|
|
|
|
dst = filepath.Join(path, dst)
|
|
|
|
}
|
2022-11-09 03:20:09 +00:00
|
|
|
stat, err := os.Stat(dst)
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return stat.IsDir()
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
2023-02-22 13:19:42 +00:00
|
|
|
|
2025-01-10 12:48:45 +00:00
|
|
|
// 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
|
2023-02-22 13:19:42 +00:00
|
|
|
srcBuf := bytes.NewBuffer(nil)
|
2025-01-10 12:48:45 +00:00
|
|
|
// 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": ""}).
|
2023-02-22 13:19:42 +00:00
|
|
|
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
|
2024-08-24 14:20:20 +00:00
|
|
|
GlobalArgs("-loglevel", "error").Silent(true).
|
|
|
|
WithOutput(srcBuf, os.Stdout)
|
|
|
|
if err = stream.Run(); err != nil {
|
2023-02-22 13:19:42 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return srcBuf, nil
|
|
|
|
}
|
2023-03-23 07:18:37 +00:00
|
|
|
|
|
|
|
func readDir(dirname string) ([]fs.FileInfo, error) {
|
|
|
|
f, err := os.Open(dirname)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
list, err := f.Readdir(-1)
|
|
|
|
f.Close()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() })
|
|
|
|
return list, nil
|
|
|
|
}
|
2023-05-11 11:57:24 +00:00
|
|
|
|
|
|
|
func (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {
|
|
|
|
fullPath := file.GetPath()
|
|
|
|
thumbPrefix := "alist_thumb_"
|
2023-07-15 08:28:55 +00:00
|
|
|
thumbName := thumbPrefix + utils.GetMD5EncodeStr(fullPath) + ".png"
|
2023-05-11 11:57:24 +00:00
|
|
|
if d.ThumbCacheFolder != "" {
|
|
|
|
// skip if the file is a thumbnail
|
|
|
|
if strings.HasPrefix(file.GetName(), thumbPrefix) {
|
|
|
|
return nil, &fullPath, nil
|
|
|
|
}
|
|
|
|
thumbPath := filepath.Join(d.ThumbCacheFolder, thumbName)
|
|
|
|
if utils.Exists(thumbPath) {
|
|
|
|
return nil, &thumbPath, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var srcBuf *bytes.Buffer
|
|
|
|
if utils.GetFileType(file.GetName()) == conf.VIDEO {
|
2025-01-10 12:48:45 +00:00
|
|
|
videoBuf, err := d.GetSnapshot(fullPath)
|
2023-05-11 11:57:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
srcBuf = videoBuf
|
|
|
|
} else {
|
|
|
|
imgData, err := os.ReadFile(fullPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
imgBuf := bytes.NewBuffer(imgData)
|
|
|
|
srcBuf = imgBuf
|
|
|
|
}
|
|
|
|
|
2023-07-15 06:31:03 +00:00
|
|
|
image, err := imaging.Decode(srcBuf, imaging.AutoOrientation(true))
|
2023-05-11 11:57:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
thumbImg := imaging.Resize(image, 144, 0, imaging.Lanczos)
|
|
|
|
var buf bytes.Buffer
|
|
|
|
err = imaging.Encode(&buf, thumbImg, imaging.PNG)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
if d.ThumbCacheFolder != "" {
|
|
|
|
err = os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), buf.Bytes(), 0666)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return &buf, nil, nil
|
|
|
|
}
|