From 37c4b45f2f19ebd7d4f9250e94872ed5b683b123 Mon Sep 17 00:00:00 2001
From: Googol Lee <i@googol.im>
Date: Tue, 17 Sep 2024 15:55:30 +0200
Subject: [PATCH] Add ffmpeg unit tests.

---
 .../executable_worker/executable_worker.go    | 92 +----------------
 .../executable_worker/ffmpeg_cli.go           | 98 +++++++++++++++++++
 .../executable_worker/ffmpeg_cli_test.go      | 90 +++++++++++++++++
 .../executable_worker/testdata/bin/ffmpeg     | 18 ++++
 .../executable_worker/testdata/bin/ffprobe    | 18 ++++
 api/scanner/media_type/media_type.go          |  2 +-
 .../processing_tasks/process_video_task.go    |  6 +-
 7 files changed, 230 insertions(+), 94 deletions(-)
 create mode 100644 api/scanner/media_encoding/executable_worker/ffmpeg_cli.go
 create mode 100644 api/scanner/media_encoding/executable_worker/ffmpeg_cli_test.go
 create mode 100755 api/scanner/media_encoding/executable_worker/testdata/bin/ffmpeg
 create mode 100755 api/scanner/media_encoding/executable_worker/testdata/bin/ffprobe

diff --git a/api/scanner/media_encoding/executable_worker/executable_worker.go b/api/scanner/media_encoding/executable_worker/executable_worker.go
index c2b9b6d..bfead7a 100644
--- a/api/scanner/media_encoding/executable_worker/executable_worker.go
+++ b/api/scanner/media_encoding/executable_worker/executable_worker.go
@@ -1,101 +1,13 @@
 package executable_worker
 
-import (
-	"fmt"
-	"log"
-	"os/exec"
-	"strings"
-
-	"github.com/photoview/photoview/api/utils"
-	"github.com/pkg/errors"
-	"gopkg.in/vansante/go-ffprobe.v2"
-)
-
 func InitializeExecutableWorkers() {
 	Magick = newMagickCli()
-	FfmpegCli = newFfmpegWorker()
+	Ffmpeg = newFfmpegCli()
 }
 
 var Magick *MagickCli = nil
-var FfmpegCli *FfmpegWorker = nil
+var Ffmpeg *FfmpegCli = nil
 
 type ExecutableWorker interface {
 	Path() string
 }
-
-type FfmpegWorker struct {
-	path string
-}
-
-func newFfmpegWorker() *FfmpegWorker {
-	if utils.EnvDisableVideoEncoding.GetBool() {
-		log.Printf("Executable worker disabled (%s=1): ffmpeg\n", utils.EnvDisableVideoEncoding.GetName())
-		return nil
-	}
-
-	path, err := exec.LookPath("ffmpeg")
-	if err != nil {
-		log.Println("Executable worker not found: ffmpeg")
-	} else {
-		version, err := exec.Command(path, "-version").Output()
-		if err != nil {
-			log.Printf("Error getting version of ffmpeg: %s\n", err)
-			return nil
-		}
-
-		log.Printf("Found executable worker: ffmpeg (%s)\n", strings.Split(string(version), "\n")[0])
-
-		return &FfmpegWorker{
-			path: path,
-		}
-	}
-
-	return nil
-}
-
-func (worker *FfmpegWorker) IsInstalled() bool {
-	return worker != nil
-}
-
-func (worker *FfmpegWorker) EncodeMp4(inputPath string, outputPath string) error {
-	args := []string{
-		"-i",
-		inputPath,
-		"-vcodec", "h264",
-		"-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(worker.path, args...)
-
-	if err := cmd.Run(); err != nil {
-		return errors.Wrapf(err, "encoding video using: %s", worker.path)
-	}
-
-	return nil
-}
-
-func (worker *FfmpegWorker) EncodeVideoThumbnail(inputPath string, outputPath string, probeData *ffprobe.ProbeData) error {
-
-	thumbnailOffsetSeconds := fmt.Sprintf("%d", int(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(worker.path, args...)
-
-	if err := cmd.Run(); err != nil {
-		return errors.Wrapf(err, "encoding video using: %s", worker.path)
-	}
-
-	return nil
-}
diff --git a/api/scanner/media_encoding/executable_worker/ffmpeg_cli.go b/api/scanner/media_encoding/executable_worker/ffmpeg_cli.go
new file mode 100644
index 0000000..0ad88a9
--- /dev/null
+++ b/api/scanner/media_encoding/executable_worker/ffmpeg_cli.go
@@ -0,0 +1,98 @@
+package executable_worker
+
+import (
+	"fmt"
+	"log"
+	"os/exec"
+	"strings"
+
+	"github.com/photoview/photoview/api/utils"
+	"gopkg.in/vansante/go-ffprobe.v2"
+)
+
+type FfmpegCli struct {
+	path string
+}
+
+func newFfmpegCli() *FfmpegCli {
+	if path, err := exec.LookPath("ffprobe"); err == nil {
+		if version, err := exec.Command(path, "-version").Output(); err == nil {
+			log.Println("Found ffprobe:", path, "version:", strings.Split(string(version), "\n")[0])
+			ffprobe.SetFFProbeBinPath(path)
+		} else {
+			log.Println("Executable ffprobe not executable:", path)
+		}
+	} else {
+		log.Println("Executable ffprobe not found")
+	}
+
+	if utils.EnvDisableVideoEncoding.GetBool() {
+		log.Printf("Executable worker disabled (%s=%q): ffmpeg\n", utils.EnvDisableVideoEncoding.GetName(), utils.EnvDisableVideoEncoding.GetValue())
+		return nil
+	}
+
+	path, err := exec.LookPath("ffmpeg")
+	if err != nil {
+		log.Println("Executable worker not found: ffmpeg")
+		return nil
+	}
+
+	version, err := exec.Command(path, "-version").Output()
+	if err != nil {
+		log.Printf("Error getting version of ffmpeg: %s\n", err)
+		return nil
+	}
+
+	log.Printf("Found executable worker: ffmpeg (%s)\n", strings.Split(string(version), "\n")[0])
+
+	return &FfmpegCli{
+		path: path,
+	}
+}
+
+func (worker *FfmpegCli) IsInstalled() bool {
+	return worker != nil
+}
+
+func (worker *FfmpegCli) EncodeMp4(inputPath string, outputPath string) error {
+	args := []string{
+		"-i",
+		inputPath,
+		"-vcodec", "h264",
+		"-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(worker.path, args...)
+
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("encoding video with %q %v error: %w", worker.path, args, err)
+	}
+
+	return nil
+}
+
+func (worker *FfmpegCli) EncodeVideoThumbnail(inputPath string, outputPath string, probeData *ffprobe.ProbeData) error {
+
+	thumbnailOffsetSeconds := fmt.Sprintf("%d", int(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(worker.path, args...)
+
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("encoding video thumbnail with %q %v error: %w", worker.path, args, err)
+	}
+
+	return nil
+}
diff --git a/api/scanner/media_encoding/executable_worker/ffmpeg_cli_test.go b/api/scanner/media_encoding/executable_worker/ffmpeg_cli_test.go
new file mode 100644
index 0000000..bd6fea7
--- /dev/null
+++ b/api/scanner/media_encoding/executable_worker/ffmpeg_cli_test.go
@@ -0,0 +1,90 @@
+package executable_worker_test
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/photoview/photoview/api/scanner/media_encoding/executable_worker"
+	"gopkg.in/vansante/go-ffprobe.v2"
+)
+
+func TestFfmpegWorkerNotExist(t *testing.T) {
+	done := setPathWithCurrent()
+	defer done()
+
+	executable_worker.InitializeExecutableWorkers()
+
+	if executable_worker.Ffmpeg.IsInstalled() {
+		t.Error("Ffmpeg should not be installed, but is found:", executable_worker.Ffmpeg)
+	}
+}
+
+func TestFfmpegWorkerIgnore(t *testing.T) {
+	donePath := setPathWithCurrent("./testdata/bin")
+	defer donePath()
+
+	doneEnv := setEnv("PHOTOVIEW_DISABLE_VIDEO_ENCODING", "true")
+	defer doneEnv()
+
+	executable_worker.InitializeExecutableWorkers()
+
+	if executable_worker.Ffmpeg.IsInstalled() {
+		t.Error("Ffmpeg should not be installed, but is found:", executable_worker.Ffmpeg)
+	}
+}
+
+func TestFfmpegWorker(t *testing.T) {
+	done := setPathWithCurrent("./testdata/bin")
+	defer done()
+
+	executable_worker.InitializeExecutableWorkers()
+
+	if !executable_worker.Ffmpeg.IsInstalled() {
+		t.Error("Ffmpeg should be installed")
+	}
+
+	t.Run("EncodeMp4Failed", func(t *testing.T) {
+		doneEnv := setEnv("FAIL_WITH", "expect failure")
+		defer doneEnv()
+
+		err := executable_worker.Ffmpeg.EncodeMp4("input_fail", "output")
+		if err == nil {
+			t.Fatalf("Ffmpeg.EncodeMp4(...) = nil, should be an error.")
+		}
+		if got, want := err.Error(), `^encoding video with ".*/testdata/bin/ffmpeg" .* 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 := executable_worker.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) {
+		doneEnv := setEnv("FAIL_WITH", "expect failure")
+		defer doneEnv()
+
+		err := executable_worker.Ffmpeg.EncodeVideoThumbnail("input_fail", "output", probeData)
+		if err == nil {
+			t.Fatalf("Ffmpeg.EncodeVideoThumbnail(...) = nil, should be an error.")
+		}
+		if got, want := err.Error(), `^encoding video thumbnail with ".*/testdata/bin/ffmpeg" .* 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 := executable_worker.Ffmpeg.EncodeVideoThumbnail("input", "output", probeData)
+		if err != nil {
+			t.Fatalf("Ffmpeg.EncodeVideoThumbnail(...) = %v, should be nil.", err)
+		}
+	})
+}
diff --git a/api/scanner/media_encoding/executable_worker/testdata/bin/ffmpeg b/api/scanner/media_encoding/executable_worker/testdata/bin/ffmpeg
new file mode 100755
index 0000000..c98e478
--- /dev/null
+++ b/api/scanner/media_encoding/executable_worker/testdata/bin/ffmpeg
@@ -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 $@
diff --git a/api/scanner/media_encoding/executable_worker/testdata/bin/ffprobe b/api/scanner/media_encoding/executable_worker/testdata/bin/ffprobe
new file mode 100755
index 0000000..3a044d2
--- /dev/null
+++ b/api/scanner/media_encoding/executable_worker/testdata/bin/ffprobe
@@ -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 $@
diff --git a/api/scanner/media_type/media_type.go b/api/scanner/media_type/media_type.go
index 83b711b..8f08d7c 100644
--- a/api/scanner/media_type/media_type.go
+++ b/api/scanner/media_type/media_type.go
@@ -264,7 +264,7 @@ func (imgType *MediaType) IsSupported() bool {
 		return true
 	}
 
-	if executable_worker.FfmpegCli.IsInstalled() && imgType.IsVideo() {
+	if executable_worker.Ffmpeg.IsInstalled() && imgType.IsVideo() {
 		return true
 	}
 
diff --git a/api/scanner/scanner_tasks/processing_tasks/process_video_task.go b/api/scanner/scanner_tasks/processing_tasks/process_video_task.go
index b29f482..8645c5d 100644
--- a/api/scanner/scanner_tasks/processing_tasks/process_video_task.go
+++ b/api/scanner/scanner_tasks/processing_tasks/process_video_task.go
@@ -94,7 +94,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
 
 		webVideoPath := path.Join(mediaCachePath, web_video_name)
 
-		err = executable_worker.FfmpegCli.EncodeMp4(video.Path, webVideoPath)
+		err = executable_worker.Ffmpeg.EncodeMp4(video.Path, webVideoPath)
 		if err != nil {
 			return []*models.MediaURL{}, errors.Wrapf(err, "could not encode mp4 video (%s)", video.Path)
 		}
@@ -139,7 +139,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
 
 		thumbImagePath := path.Join(mediaCachePath, video_thumb_name)
 
-		err = executable_worker.FfmpegCli.EncodeVideoThumbnail(video.Path, thumbImagePath, probeData)
+		err = executable_worker.Ffmpeg.EncodeVideoThumbnail(video.Path, thumbImagePath, probeData)
 		if err != nil {
 			return []*models.MediaURL{}, errors.Wrapf(err, "failed to generate thumbnail for video (%s)", video.Title)
 		}
@@ -177,7 +177,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
 			fmt.Printf("Video thumbnail found in database but not in cache, re-encoding photo to cache: %s\n", videoThumbnailURL.MediaName)
 			updatedURLs = append(updatedURLs, videoThumbnailURL)
 
-			err = executable_worker.FfmpegCli.EncodeVideoThumbnail(video.Path, thumbImagePath, probeData)
+			err = executable_worker.Ffmpeg.EncodeVideoThumbnail(video.Path, thumbImagePath, probeData)
 			if err != nil {
 				return []*models.MediaURL{}, errors.Wrapf(err, "failed to generate thumbnail for video (%s)", video.Title)
 			}
