alist/drivers/local/util.go
2025-11-11 21:54:19 +08:00

237 lines
6.7 KiB
Go

package local
import (
"bytes"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"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"
ffmpeg "github.com/u2takey/ffmpeg-go"
)
func isLinkedDir(f fs.FileInfo, path string) bool {
if f.Mode()&os.ModeSymlink == os.ModeSymlink || (runtime.GOOS == "windows" && f.Mode()&os.ModeIrregular != 0) {
dst, err := os.Readlink(path)
if err != nil {
return false
}
if !filepath.IsAbs(dst) {
dst = filepath.Join(filepath.Dir(path), dst)
}
dst, err = filepath.Abs(dst)
if err != nil {
return false
}
stat, err := os.Stat(dst)
if err != nil {
return false
}
return stat.IsDir()
}
return false
}
// resizeImageToBufferWithFFmpegGo 使用 ffmpeg-go 调整图片大小并输出到内存缓冲区
func resizeImageToBufferWithFFmpegGo(inputFile string, width int, outputFormat string /* e.g., "image2pipe", "png_pipe", "mjpeg" */) (*bytes.Buffer, error) {
outBuffer := bytes.NewBuffer(nil)
// Determine codec based on desired output format for piping
// For generic image piping, 'image2' is often used with -f image2pipe
// For specific formats to buffer, you might specify the codec directly
var vcodec string
switch outputFormat {
case "png_pipe": // if you want to ensure PNG format in buffer
vcodec = "png"
case "mjpeg": // if you want to ensure JPEG format in buffer
vcodec = "mjpeg"
// default or "image2pipe" could leave codec choice more to ffmpeg or require -c:v later
}
outputArgs := ffmpeg.KwArgs{
"vf": fmt.Sprintf("scale=%d:-1:flags=lanczos,format=yuv444p", width),
"vframes": "1",
"f": outputFormat, // Format for piping (e.g., image2pipe, png_pipe)
}
if vcodec != "" {
outputArgs["vcodec"] = vcodec
}
if outputFormat == "mjpeg" {
outputArgs["q:v"] = "3"
}
err := ffmpeg.Input(inputFile).
Output("pipe:", outputArgs). // Output to pipe (stdout)
GlobalArgs("-loglevel", "error").
Silent(true). // Suppress ffmpeg's own console output
WithOutput(outBuffer, os.Stderr). // Capture stdout to outBuffer, stderr to os.Stderr
// ErrorToStdOut(). // Alternative: send ffmpeg's stderr to Go's stdout
Run()
if err != nil {
return nil, fmt.Errorf("ffmpeg-go failed to resize image %s to buffer: %w", inputFile, err)
}
if outBuffer.Len() == 0 {
return nil, fmt.Errorf("ffmpeg-go produced empty buffer for %s", inputFile)
}
return outBuffer, nil
}
func generateThumbnailWithImagingOptimized(imagePath string, targetWidth int, quality int) (*bytes.Buffer, error) {
file, err := os.Open(imagePath)
if err != nil {
return nil, fmt.Errorf("failed to open image: %w", err)
}
defer file.Close()
img, err := imaging.Decode(file, imaging.AutoOrientation(true))
if err != nil {
return nil, fmt.Errorf("failed to decode image: %w", err)
}
thumbImg := imaging.Resize(img, targetWidth, 0, imaging.Lanczos)
img = nil
var buf bytes.Buffer
// imaging.Encode
// imaging.PNG, imaging.JPEG, imaging.GIF, imaging.BMP, imaging.TIFF
outputFormat := imaging.JPEG
encodeOptions := []imaging.EncodeOption{imaging.JPEGQuality(quality)}
// outputFormat := imaging.PNG
// encodeOptions := []imaging.EncodeOption{}
err = imaging.Encode(&buf, thumbImg, outputFormat, encodeOptions...)
if err != nil {
return nil, fmt.Errorf("failed to encode thumbnail: %w", err)
}
thumbImg = nil
return &buf, nil
}
// 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 d.videoThumbPosIsPercentage {
ss = fmt.Sprintf("%f", totalDuration*d.videoThumbPos)
} else {
// If the value is greater than the total duration, use the total duration
if d.videoThumbPos > totalDuration {
ss = fmt.Sprintf("%f", totalDuration)
} else {
ss = fmt.Sprintf("%f", d.videoThumbPos)
}
}
// Run ffmpeg to get the snapshot
srcBuf := bytes.NewBuffer(nil)
// 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", "vf": fmt.Sprintf("scale=%d:-1:flags=lanczos", d.thumbPixel)}).
GlobalArgs("-loglevel", "error").Silent(true).
WithOutput(srcBuf, os.Stdout)
if err = stream.Run(); err != nil {
return nil, err
}
return srcBuf, nil
}
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
}
func (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {
fullPath := file.GetPath()
thumbPrefix := "alist_thumb_"
thumbName := thumbPrefix + utils.GetMD5EncodeStr(fullPath) + ".png"
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 {
videoBuf, err := d.GetSnapshot(fullPath)
if err != nil {
return nil, nil, err
}
srcBuf = videoBuf
} else {
if d.useFFmpeg {
imgData, err := resizeImageToBufferWithFFmpegGo(fullPath, d.thumbPixel, "image2pipe")
srcBuf = imgData
if err != nil {
return nil, nil, err
}
} else {
imgData, err := generateThumbnailWithImagingOptimized(fullPath, d.thumbPixel, 70)
srcBuf = imgData
if err != nil {
return nil, nil, err
}
}
}
if d.ThumbCacheFolder != "" {
err := os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), srcBuf.Bytes(), 0666)
if err != nil {
return nil, nil, err
}
}
return srcBuf, nil, nil
}