From 969af2ab6985f900f750c50d18f2c58fb3012527 Mon Sep 17 00:00:00 2001 From: A Ghoul Coder Date: Tue, 11 Jul 2023 07:53:53 +0200 Subject: [PATCH] add phasher (#3864) * add phasher A simple `phasher` program that accepts a video file as a command line argument and calculates and prints its PHASH. The goal of this separate executable is to have a simple way to calculate phashes that doesn't depend on a full stash instance so that third-party systems and tools can independently generate PHASHes which can be used for interacting with stash and stash-box APIs and data. Currently `phasher` is built in the default make target along with `stash` by simply running `make`. Cross-platform targets have not been considered. Concurrency is intentionally not implemented because it is simpler to use [GNU Parallel](https://www.gnu.org/software/parallel/). For example: ``` parallel phasher {} ::: *.mp4 ``` * standard dir structure for phasher and separate make target The make target still needs to be integrated into the rest of the Makefile so it can be built as part of normal releases. * phasher: basic usage output and quiet option * phasher: allow and process multiple command line arguments * phasher: camelCase identifiers * phasher: initialize ffmpeg and ffprobe only once --- .gitignore | 3 +- Makefile | 6 ++++ cmd/phasher/main.go | 83 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 cmd/phasher/main.go diff --git a/.gitignore b/.gitignore index 7b70f7306..ead0b09f9 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ node_modules *.db /stash +/phasher dist .DS_Store -/.local* \ No newline at end of file +/.local* diff --git a/Makefile b/Makefile index 3fd45a307..5646f94bf 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,12 @@ build: build-flags build: go build $(OUTPUT) $(BUILD_FLAGS) ./cmd/stash +# TODO: Integrate the phasher target with the rest of the Makefile, +# TODO: so it can be built as part of normal releases. +.PHONY: phasher +phasher: + go build -o $@ -trimpath -buildmode=pie -ldflags '-s -w' ./cmd/phasher + # builds a dynamically-linked release binary .PHONY: build-release build-release: LDFLAGS += -s -w diff --git a/cmd/phasher/main.go b/cmd/phasher/main.go new file mode 100644 index 000000000..f4648b74e --- /dev/null +++ b/cmd/phasher/main.go @@ -0,0 +1,83 @@ +// TODO: document in README.md +package main + +import ( + "context" + "fmt" + "os" + + flag "github.com/spf13/pflag" + "github.com/stashapp/stash/pkg/ffmpeg" + "github.com/stashapp/stash/pkg/file" + "github.com/stashapp/stash/pkg/hash/videophash" +) + +func customUsage() { + fmt.Fprintf(os.Stderr, "Usage:\n") + fmt.Fprintf(os.Stderr, "%s [OPTIONS] VIDEOFILE...\n\nOptions:\n", os.Args[0]) + flag.PrintDefaults() +} + +func printPhash(ff *ffmpeg.FFMpeg, ffp ffmpeg.FFProbe, inputfile string, quiet *bool) error { + ffvideoFile, err := ffp.NewVideoFile(inputfile) + if err != nil { + return err + } + + // All we need for videophash.Generate() is + // videoFile.Path (from BaseFile) + // videoFile.Duration + // The rest of the struct isn't needed. + vf := &file.VideoFile{ + BaseFile: &file.BaseFile{Path: inputfile}, + Duration: ffvideoFile.FileDuration, + } + + phash, err := videophash.Generate(ff, vf) + if err != nil { + return err + } + + if *quiet { + fmt.Printf("%x\n", *phash) + } else { + fmt.Printf("%x %v\n", *phash, vf.Path) + } + return nil +} + +func main() { + flag.Usage = customUsage + quiet := flag.BoolP("quiet", "q", false, "print only the phash") + help := flag.BoolP("help", "h", false, "print this help output") + flag.Parse() + + if *help { + flag.Usage() + os.Exit(2) + } + + args := flag.Args() + + if len(args) < 1 { + fmt.Fprintf(os.Stderr, "Missing VIDEOFILE argument.\n") + flag.Usage() + os.Exit(2) + } + + if len(args) > 1 { + fmt.Fprintln(os.Stderr, "Files will be processed sequentially! Consier using GNU Parallel.") + fmt.Fprintf(os.Stderr, "Example: parallel %v ::: *.mp4\n", os.Args[0]) + } + + ffmpegPath, ffprobePath := ffmpeg.GetPaths(nil) + encoder := ffmpeg.NewEncoder(ffmpegPath) + encoder.InitHWSupport(context.TODO()) + ffprobe := ffmpeg.FFProbe(ffprobePath) + + for _, item := range args { + if err := printPhash(encoder, ffprobe, item, quiet); err != nil { + fmt.Fprintln(os.Stderr, err) + } + } +}