精简后的项目:仅保留 api/ui 源码、文档、构建依赖

This commit is contained in:
gallery
2026-09-11 16:11:09 +08:00
commit 490e85634e
457 changed files with 86862 additions and 0 deletions
@@ -0,0 +1,53 @@
package executable_worker
import (
"errors"
"fmt"
"os/exec"
"strings"
"github.com/photoview/photoview/api/log"
"gopkg.in/vansante/go-ffprobe.v2"
)
var ErrNoDependency = errors.New("dependency not found")
var ErrDisabledFunction = errors.New("function disabled")
// Initialize Initializes all workers. It returns a function to terminate workers, which should be called before the program closing.
func Initialize() func() {
Magick = newMagickWand()
Ffmpeg = newFfmpegCli()
if err := SetFfprobePath(); err != nil {
log.Error(nil, "Init ffprobe fail.", "error", err)
}
return func() {
Magick.Terminate()
Magick = nil
}
}
var Magick *MagickWand = nil
var Ffmpeg *FfmpegCli = nil
type ExecutableWorker interface {
Path() string
}
func SetFfprobePath() error {
path, err := exec.LookPath("ffprobe")
if err != nil {
return fmt.Errorf("Executable ffprobe not found: %w", err)
}
version, err := exec.Command(path, "-version").Output()
if err != nil {
return fmt.Errorf("Executable ffprobe(%q) not executable: %w", path, err)
}
log.Info(nil, "Found ffprobe", "path", path, "version", strings.Split(string(version), "\n")[0])
ffprobe.SetFFProbeBinPath(path)
return nil
}
@@ -0,0 +1,59 @@
package executable_worker
import (
"path/filepath"
"runtime"
"strings"
"testing"
_ "github.com/photoview/photoview/api/test_utils/flags"
)
const testdataBinPath = "./test_data/mock_bin"
// SetPathWithCurrent sets PATH env to `paths` in the directory of testing files. The PATH will restore to the previous value when the test is done.
func SetPathWithCurrent(t *testing.T, paths ...string) {
_, file, _, ok := runtime.Caller(1)
if !ok {
t.Log("Can't get the test file. Ignore setting PATH.")
return
}
base := filepath.Dir(file)
for i, path := range paths {
paths[i] = filepath.Join(base, path)
}
t.Setenv("PATH", strings.Join(paths, ":"))
}
func TestInitFfprobePath(t *testing.T) {
t.Run("PathFail", func(t *testing.T) {
SetPathWithCurrent(t, "non_exist_path")
err := SetFfprobePath()
if err == nil {
t.Fatalf("InitFfprobePath() returns nil, want an error")
}
})
t.Run("VersionFail", func(t *testing.T) {
SetPathWithCurrent(t, testdataBinPath)
t.Setenv("FAIL_WITH", "expect failure")
err := SetFfprobePath()
if err == nil {
t.Fatalf("InitFfprobePath() returns nil, want an error")
}
})
t.Run("Succeed", func(t *testing.T) {
SetPathWithCurrent(t, testdataBinPath)
err := SetFfprobePath()
if err != nil {
t.Fatalf("InitFfprobePath() returns %v, want nil", err)
}
})
}
@@ -0,0 +1,122 @@
package executable_worker
import (
"fmt"
"os/exec"
"strings"
"github.com/photoview/photoview/api/log"
"github.com/photoview/photoview/api/utils"
"gopkg.in/vansante/go-ffprobe.v2"
)
const defaultCodec = "h264"
var hwAccToCodec = map[string]string{
"qsv": defaultCodec + "_qsv",
"vaapi": defaultCodec + "_vaapi",
"nvenc": defaultCodec + "_nvenc",
}
type FfmpegCli struct {
path string
videoCodec string
err error
}
func newFfmpegCli() *FfmpegCli {
if utils.EnvDisableVideoEncoding.GetBool() {
log.Warn(nil, "Executable ffmpeg worker disabled", utils.EnvDisableVideoEncoding.GetName(), utils.EnvDisableVideoEncoding.GetValue())
return &FfmpegCli{
err: ErrDisabledFunction,
}
}
path, err := exec.LookPath("ffmpeg")
if err != nil {
log.Error(nil, "Executable ffmpeg worker not found")
return &FfmpegCli{
err: ErrNoDependency,
}
}
version, err := exec.Command(path, "-version").Output()
if err != nil {
log.Error(nil, "Executable ffmpeg worker getting version error", "error", err)
return &FfmpegCli{
err: ErrNoDependency,
}
}
hwAcc := utils.EnvVideoHardwareAcceleration.GetValue()
codec, ok := hwAccToCodec[hwAcc]
if !ok {
if strings.HasPrefix(hwAcc, "_") {
// A secret way to set the codec directly.
codec = hwAcc[1:]
} else {
codec = defaultCodec
}
}
log.Info(nil, "Found executable worker: ffmpeg", "version", strings.Split(string(version), "\n")[0], "codec", codec)
return &FfmpegCli{
path: path,
videoCodec: codec,
}
}
func (cli *FfmpegCli) IsInstalled() bool {
return cli.err == nil
}
func (cli *FfmpegCli) EncodeMp4(inputPath string, outputPath string) error {
if cli.err != nil {
return fmt.Errorf("encoding video %q error: ffmpeg: %w", inputPath, cli.err)
}
args := []string{
"-i",
inputPath,
"-vcodec", cli.videoCodec,
"-acodec", "aac",
"-vf", "scale='min(1080,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2",
"-movflags", "+faststart+use_metadata_tags",
outputPath,
}
cmd := exec.Command(cli.path, args...)
if err := cmd.Run(); err != nil {
return fmt.Errorf("encoding video with %q %v error: %w", cli.path, args, err)
}
return nil
}
func (cli *FfmpegCli) EncodeVideoThumbnail(inputPath string, outputPath string, probeData *ffprobe.ProbeData) error {
if cli.err != nil {
return fmt.Errorf("encoding video thumbnail %q error: ffmpeg: %w", inputPath, cli.err)
}
thumbnailOffsetSeconds := fmt.Sprintf("%.f", probeData.Format.DurationSeconds*0.25)
args := []string{
"-ss", thumbnailOffsetSeconds, // grab frame at time offset
"-i",
inputPath,
"-vframes", "1", // output one frame
"-an", // disable audio
"-vf", "scale='min(1024,iw)':'min(1024,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2",
outputPath,
}
cmd := exec.Command(cli.path, args...)
if err := cmd.Run(); err != nil {
return fmt.Errorf("encoding video thumbnail with %q %v error: %w", cli.path, args, err)
}
return nil
}
@@ -0,0 +1,165 @@
package executable_worker
import (
"errors"
"regexp"
"testing"
"github.com/photoview/photoview/api/utils"
"gopkg.in/vansante/go-ffprobe.v2"
)
func TestFfmpegNotExist(t *testing.T) {
SetPathWithCurrent(t, "")
Ffmpeg = newFfmpegCli()
if got, want := Ffmpeg.err, ErrNoDependency; got != want {
t.Errorf("Ffmpeg.err = %v, want: %v", got, want)
}
if Ffmpeg.IsInstalled() {
t.Error("Ffmpeg should not be installed, but is found:", Ffmpeg)
}
if got, want := Ffmpeg.EncodeMp4("input", "output"), ErrNoDependency; !errors.Is(got, want) {
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
}
if got, want := Ffmpeg.EncodeVideoThumbnail("input", "output", nil), ErrNoDependency; !errors.Is(got, want) {
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
}
}
func TestFfmpegVersionFail(t *testing.T) {
SetPathWithCurrent(t, testdataBinPath)
t.Setenv("FAIL_WITH", "expect failure")
Ffmpeg = newFfmpegCli()
if got, want := Ffmpeg.err, ErrNoDependency; got != want {
t.Errorf("Ffmpeg.err = %v, want: %v", got, want)
}
if Ffmpeg.IsInstalled() {
t.Error("Ffmpeg should not be installed, but is found:", Ffmpeg)
}
if got, want := Ffmpeg.EncodeMp4("input", "output"), ErrNoDependency; !errors.Is(got, want) {
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
}
if got, want := Ffmpeg.EncodeVideoThumbnail("input", "output", nil), ErrNoDependency; !errors.Is(got, want) {
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
}
}
func TestFfmpegIgnore(t *testing.T) {
SetPathWithCurrent(t, testdataBinPath)
t.Setenv("PHOTOVIEW_DISABLE_VIDEO_ENCODING", "true")
Ffmpeg = newFfmpegCli()
if got, want := Ffmpeg.err, ErrDisabledFunction; got != want {
t.Errorf("Ffmpeg.err = %v, want: %v", got, want)
}
if Ffmpeg.IsInstalled() {
t.Error("Ffmpeg should be ignored (as it is disabled), but is initialized:", Ffmpeg)
}
if got, want := Ffmpeg.EncodeMp4("input", "output"), ErrDisabledFunction; !errors.Is(got, want) {
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
}
if got, want := Ffmpeg.EncodeVideoThumbnail("input", "output", nil), ErrDisabledFunction; !errors.Is(got, want) {
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
}
}
func TestFfmpeg(t *testing.T) {
SetPathWithCurrent(t, testdataBinPath)
Ffmpeg = newFfmpegCli()
if !Ffmpeg.IsInstalled() {
t.Fatal("Ffmpeg should be installed")
}
t.Run("EncodeMp4Failed", func(t *testing.T) {
t.Setenv("FAIL_WITH", "expect failure")
err := Ffmpeg.EncodeMp4("input", "output")
if err == nil {
t.Fatalf("Ffmpeg.EncodeMp4(...) = nil, should be an error.")
}
if got, want := err.Error(), `^encoding video with ".*/test_data/mock_bin/ffmpeg" \[-i input -vcodec h264 .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) {
t.Errorf("Ffmpeg.EncodeMp4(...) = %q, should be as reg pattern %q", got, want)
}
})
t.Run("EncodeMp4Succeeded", func(t *testing.T) {
err := Ffmpeg.EncodeMp4("input", "output")
if err != nil {
t.Fatalf("Ffmpeg.EncodeMp4(...) = %v, should be nil.", err)
}
})
probeData := &ffprobe.ProbeData{
Format: &ffprobe.Format{
DurationSeconds: 10,
},
}
t.Run("EncodeVideoThumbnailMp4Failed", func(t *testing.T) {
t.Setenv("FAIL_WITH", "expect failure")
err := Ffmpeg.EncodeVideoThumbnail("input", "output", probeData)
if err == nil {
t.Fatalf("Ffmpeg.EncodeVideoThumbnail(...) = nil, should be an error.")
}
if got, want := err.Error(), `^encoding video thumbnail with ".*/test_data/mock_bin/ffmpeg" \[-ss 2 -i input .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) {
t.Errorf("Ffmpeg.EncodeVideoThumbnail(...) = %q, should be as reg pattern %q", got, want)
}
})
t.Run("EncodeVideoThumbnailSucceeded", func(t *testing.T) {
err := Ffmpeg.EncodeVideoThumbnail("input", "output", probeData)
if err != nil {
t.Fatalf("Ffmpeg.EncodeVideoThumbnail(...) = %v, should be nil.", err)
}
})
}
func TestFfmpegWithHWAcc(t *testing.T) {
SetPathWithCurrent(t, testdataBinPath)
t.Setenv(utils.EnvVideoHardwareAcceleration.GetName(), "qsv")
Ffmpeg = newFfmpegCli()
t.Setenv("FAIL_WITH", "expect failure")
err := Ffmpeg.EncodeMp4("input", "output")
if err == nil {
t.Fatalf("Ffmpeg.EncodeMp4(...) = nil, should be an error.")
}
if got, want := err.Error(), `^encoding video with ".*/test_data/mock_bin/ffmpeg" \[-i input -vcodec h264_qsv .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) {
t.Errorf("Ffmpeg.EncodeMp4(...) = %q, should be as reg pattern %q", got, want)
}
}
func TestFfmpegWithCustomCodec(t *testing.T) {
SetPathWithCurrent(t, testdataBinPath)
t.Setenv(utils.EnvVideoHardwareAcceleration.GetName(), "_custom")
Ffmpeg = newFfmpegCli()
t.Setenv("FAIL_WITH", "expect failure")
err := Ffmpeg.EncodeMp4("input", "output")
if err == nil {
t.Fatalf("Ffmpeg.EncodeMp4(...) = nil, should be an error.")
}
if got, want := err.Error(), `^encoding video with ".*/test_data/mock_bin/ffmpeg" \[-i input -vcodec custom .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) {
t.Errorf("Ffmpeg.EncodeMp4(...) = %q, should be as reg pattern %q", got, want)
}
}
@@ -0,0 +1,118 @@
package executable_worker
import (
"fmt"
"github.com/photoview/photoview/api/log"
"gopkg.in/gographics/imagick.v3/imagick"
)
type MagickWand struct {
initialized bool
}
func newMagickWand() *MagickWand {
imagick.Initialize()
verstr, vernum := imagick.GetVersion()
log.Info(nil, "Found magickwand worker: "+verstr, "version", vernum)
return &MagickWand{
initialized: true,
}
}
func (cli *MagickWand) Terminate() {
cli.initialized = false
imagick.Terminate()
}
func (cli *MagickWand) IsInstalled() bool {
return cli != nil && cli.initialized
}
func (cli *MagickWand) EncodeJpeg(inputPath string, outputPath string, jpegQuality uint) error {
wand, err := cli.createWandFromFile(inputPath)
if err != nil {
return err
}
defer wand.Destroy()
if err := wand.SetFormat("JPEG"); err != nil {
return fmt.Errorf("ImagickWand set JPEG format for %q error: %w", inputPath, err)
}
if err := wand.SetImageCompressionQuality(jpegQuality); err != nil {
return fmt.Errorf("ImagickWand set JPEG quality %d for %q error: %w", jpegQuality, inputPath, err)
}
if err := wand.WriteImage(outputPath); err != nil {
return fmt.Errorf("ImagickWand write %q error: %w", outputPath, err)
}
return nil
}
func (cli *MagickWand) GenerateThumbnail(inputPath string, outputPath string, width, height uint) error {
wand, err := cli.createWandFromFile(inputPath)
if err != nil {
return err
}
defer wand.Destroy()
if err := wand.ThumbnailImage(width, height); err != nil {
return fmt.Errorf("ImagickWand generate thumbnail for %q error: %w", inputPath, err)
}
if err := wand.SetFormat("JPEG"); err != nil {
return fmt.Errorf("ImagickWand set JPEG format for %q error: %w", inputPath, err)
}
if err := wand.SetImageCompressionQuality(70); err != nil {
return fmt.Errorf("ImagickWand set JPEG quality %d for %q error: %w", 70, inputPath, err)
}
if err := wand.WriteImage(outputPath); err != nil {
return fmt.Errorf("ImagickWand write %q error: %w", outputPath, err)
}
return nil
}
func (cli *MagickWand) IdentifyDimension(inputPath string) (width, height uint, reterr error) {
wand, err := cli.createWandFromFile(inputPath)
if err != nil {
reterr = err
return
}
defer wand.Destroy()
width = wand.GetImageWidth()
height = wand.GetImageHeight()
return
}
func (cli *MagickWand) createWandFromFile(inputPath string) (*imagick.MagickWand, error) {
if !cli.IsInstalled() {
return nil, fmt.Errorf("ImagickWand is not initialized")
}
wand := imagick.NewMagickWand()
if err := wand.ReadImage(inputPath); err != nil {
return nil, fmt.Errorf("ImagickWand read %q error: %w", inputPath, err)
}
if err := wand.AutoOrientImage(); err != nil {
return nil, fmt.Errorf("ImagickWand auto-orient %q error: %w", inputPath, err)
}
// Reset EXIF orientation to 1 (top-left) since image is now properly oriented
if err := wand.SetImageOrientation(imagick.ORIENTATION_TOP_LEFT); err != nil {
return nil, fmt.Errorf("ImagickWand set orientation for %q error: %w", inputPath, err)
}
return wand, nil
}
@@ -0,0 +1,18 @@
#!/bin/sh
set -eu
: ${FAIL_WITH=""}
case "$1" in
"--version")
echo ffmpeg: version fake
;;
esac
if [ "${FAIL_WITH}" != "" ]
then
echo ${FAIL_WITH}
exit -1
fi
echo $@
@@ -0,0 +1,18 @@
#!/bin/sh
set -eu
: ${FAIL_WITH=""}
case "$1" in
"--version")
echo ffprobe: version fake
;;
esac
if [ "${FAIL_WITH}" != "" ]
then
echo ${FAIL_WITH}
exit -1
fi
echo $@
@@ -0,0 +1,35 @@
#!/bin/sh
set -eu
: ${FAIL_WITH=""}
: ${INVALID_OUTPUT=""}
if [ "${FAIL_WITH}" != "" ]
then
echo ${FAIL_WITH}
exit -1
fi
if [ "$#" = "0" ] # no args
then
exit 0
fi
case "$1" in
"-version")
echo magick: version fake
exit 0
;;
"identify")
if [ "${INVALID_OUTPUT}" != "" ]
then
echo -n ${INVALID_OUTPUT}
exit 0
fi
echo -n '{"height":800, "width":1000}'
exit 0
;;
esac
echo $@