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 }