diff --git a/app/app.go b/app/app.go index ee6b14d..06f0d9c 100644 --- a/app/app.go +++ b/app/app.go @@ -22,7 +22,7 @@ const ( // supportedDefaultOptions contains those flags that can be // overridden through the `F2_DEFAULT_OPTS` environmental variable. var supportedDefaultOptions = []string{ - "hidden", "allow-overwrites", "exclude", "exec", "fix-conflicts", "include-dir", "ignore-case", "ignore-ext", "interactive", "json", "max-depth", "no-color", "only-dir", "quiet", "recursive", "replace-limit", "sort", "sortr", "string-mode", "verbose", + "hidden", "allow-overwrites", "exclude", "exec", "fix-conflicts", "include-dir", "ignore-case", "ignore-ext", "interactive", "json", "max-depth", "no-color", "only-dir", "quiet", "recursive", "replace-limit", "sort", "sortr", "string-mode", "verbose", "exiftool-opts", } func init() { @@ -85,7 +85,7 @@ func getDefaultOptsCtx() *cli.Context { } // Run needs to be called here so that `defaultCtx` is populated - // It errors out when + // It errors out when // TODO: complete this err := app.Run(defaultOpts) if err != nil { // TODO: Decide what to do here @@ -231,6 +231,10 @@ or: FIND [REPLACE] [PATHS TO FILES AND DIRECTORIES...]` DefaultText: "", TakesFile: true, }, + &cli.StringFlag{ + Name: "exiftool-opts", + Usage: "Provide custom options when using ExifTool variables", + }, &cli.StringSliceFlag{ Name: "find", Aliases: []string{"f"}, diff --git a/go.mod b/go.mod index 8c71771..4eef5ad 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,14 @@ go 1.21 require ( github.com/adrg/xdg v0.4.0 - github.com/barasher/go-exiftool v1.8.0 + github.com/barasher/go-exiftool v1.10.0 github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086 github.com/google/go-cmp v0.5.9 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/pterm/pterm v0.12.46 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/urfave/cli/v2 v2.4.10 - golang.org/x/sys v0.16.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.3.7 gopkg.in/djherbis/times.v1 v1.3.0 ) @@ -32,6 +32,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/djherbis/times v1.6.0 // indirect github.com/gookit/color v1.5.2 // indirect + github.com/jessevdk/go-flags v1.5.0 // indirect github.com/lithammer/fuzzysearch v1.1.5 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index a8e957e..3df72b6 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoU github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/barasher/go-exiftool v1.8.0 h1:u8bEi1mhLtpVC5aG/ZJlRS/r+SkK+rcgbZQwcKUb424= github.com/barasher/go-exiftool v1.8.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo= +github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs= +github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= @@ -36,6 +38,8 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -99,6 +103,7 @@ golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f h1:Al51T6tzvuh3oiwX11vex3QgJ golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -109,6 +114,8 @@ golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/internal/config/config.go b/internal/config/config.go index 1e358c5..4394687 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/jessevdk/go-flags" + "github.com/kballard/go-shellquote" "github.com/urfave/cli/v2" ) @@ -26,6 +28,15 @@ var ( var conf *Config +// ExiftoolOpts defines supported options for customizing Exitool's output +type ExiftoolOpts struct { + API string `long:"api"` // corresponds to the `-api` flag + Charset string `long:"charset"` // corresponds to the `-charset` flag + CoordFormat string `long:"coordFormat"` // corresponds to the `-coordFormat` flag + DateFormat string `long:"dateFormat"` // corresponds to the `-dateFormat` flag + ExtractEmbedded bool `long:"extractEmbedded"` // corresponds to the `-extractEmbedded` flag +} + // Config represents the program configuration. type Config struct { Date time.Time @@ -62,6 +73,7 @@ type Config struct { SimpleMode bool JSON bool Interactive bool + ExiftoolOpts ExiftoolOpts } // SetFindStringRegex compiles a regular expression for the @@ -109,6 +121,18 @@ func (c *Config) setOptions(ctx *cli.Context) error { c.Revert = ctx.Bool("undo") c.FilesAndDirPaths = ctx.Args().Slice() + if len(ctx.String("exiftool-opts")) != 0 { + args, err := shellquote.Split(ctx.String("exiftool-opts")) + if err != nil { + return err + } + + _, err = flags.ParseArgs(&c.ExiftoolOpts, args) + if err != nil { + return err + } + } + // Default to the current working directory if no path arguments are provided if len(c.FilesAndDirPaths) == 0 { c.FilesAndDirPaths = append(c.FilesAndDirPaths, ".") diff --git a/replace/replace_test/testdata/binary.mp3 b/replace/replace_test/testdata/binary.mp3 new file mode 100644 index 0000000..67f4fb3 Binary files /dev/null and b/replace/replace_test/testdata/binary.mp3 differ diff --git a/replace/replace_test/testdata/embedded.mp4 b/replace/replace_test/testdata/embedded.mp4 new file mode 100644 index 0000000..5696e24 Binary files /dev/null and b/replace/replace_test/testdata/embedded.mp4 differ diff --git a/replace/replace_test/testdata/gps.jpg b/replace/replace_test/testdata/gps.jpg new file mode 100644 index 0000000..a6d4ef5 Binary files /dev/null and b/replace/replace_test/testdata/gps.jpg differ diff --git a/replace/replace_test/variables_test.go b/replace/replace_test/variables_test.go index f9405e9..d966f2c 100644 --- a/replace/replace_test/variables_test.go +++ b/replace/replace_test/variables_test.go @@ -255,6 +255,111 @@ func TestVariables(t *testing.T) { "{btime.YYYY}-{ctime.MM}-{now.DD}{ext}", }, }, + { + Name: "replace GPSPosition Exiftool tag using default settings", + Changes: []*file.Change{ + { + BaseDir: "testdata", + Source: "gps.jpg", + }, + }, + Want: []string{ + `testdata/43 deg 28' 2.81" N, 11 deg 53' 6.46" E.jpg`, + }, + Args: []string{ + "-r", "{xt.GPSPosition}{ext}", + }, + }, + { + Name: "use --coordFormat Exiftool option to customize GPS format", + Changes: []*file.Change{ + { + BaseDir: "testdata", + Source: "gps.jpg", + }, + }, + Want: []string{ + "testdata/+43.467448, +11.885127.jpg", + }, + Args: []string{ + "-r", "{xt.GPSPosition}{ext}", "--exiftool-opts", `--coordFormat %+f`, + }, + }, + { + Name: "use Exiftool GPSDateTime tag default format", + Changes: []*file.Change{ + { + BaseDir: "testdata", + Source: "gps.jpg", + }, + }, + Want: []string{ + "testdata/2008:10:23 14:27:07.24Z.jpg", + }, + Args: []string{ + "-r", "{xt.GPSDateTime}{ext}", + }, + }, + { + Name: "use --dateFormat Exiftool option to customize date format", + Changes: []*file.Change{ + { + BaseDir: "testdata", + Source: "gps.jpg", + }, + }, + Want: []string{ + "testdata/2008-10-23.jpg", + }, + Args: []string{ + "-r", "{xt.GPSDateTime}{ext}", "--exiftool-opts", `--dateFormat %Y-%m-%d`, + }, + }, + { + Name: "use --api Exiftool option to customize date format", + Changes: []*file.Change{ + { + BaseDir: "testdata", + Source: "gps.jpg", + }, + }, + Want: []string{ + "testdata/2008-10-23.jpg", + }, + Args: []string{ + "-r", "{xt.GPSDateTime}{ext}", "--exiftool-opts", `--api DateFormat=%Y-%m-%d`, + }, + }, + { + Name: "fail to find OtherSerialNumber tag without --extractEmbedded Exiftool option", + Changes: []*file.Change{ + { + BaseDir: "testdata", + Source: "embedded.mp4", + }, + }, + Want: []string{ + "testdata/.mp4", + }, + Args: []string{ + "-r", "{xt.OtherSerialNumber}{ext}", + }, + }, + { + Name: "find OtherSerialNumber tag with --extractEmbedded Exiftool option", + Changes: []*file.Change{ + { + BaseDir: "testdata", + Source: "embedded.mp4", + }, + }, + Want: []string{ + "testdata/HERO4 Silver.mp4", + }, + Args: []string{ + "-r", "{xt.OtherSerialNumber}{ext}", "--exiftool-opts", `--extractEmbedded`, + }, + }, } replaceTest(t, testCases) diff --git a/replace/variables.go b/replace/variables.go index 74b268c..c2e2bb3 100644 --- a/replace/variables.go +++ b/replace/variables.go @@ -611,7 +611,34 @@ func replaceExifToolVars( target, sourcePath string, xtVars exiftoolVars, ) (string, error) { - et, err := exiftool.NewExiftool() + conf := config.Get() + + var opts []func(*exiftool.Exiftool) error + + if conf.ExiftoolOpts.API != "" { + opts = append(opts, exiftool.Api(conf.ExiftoolOpts.API)) + } + + if conf.ExiftoolOpts.Charset != "" { + opts = append(opts, exiftool.Charset(conf.ExiftoolOpts.Charset)) + } + + if conf.ExiftoolOpts.CoordFormat != "" { + opts = append( + opts, + exiftool.CoordFormant(conf.ExiftoolOpts.CoordFormat), + ) + } + + if conf.ExiftoolOpts.DateFormat != "" { + opts = append(opts, exiftool.DateFormant(conf.ExiftoolOpts.DateFormat)) + } + + if conf.ExiftoolOpts.ExtractEmbedded { + opts = append(opts, exiftool.ExtractEmbedded()) + } + + et, err := exiftool.NewExiftool(opts...) if err != nil { return "", fmt.Errorf("Failed to initialise exiftool: %w", err) } @@ -634,6 +661,7 @@ func replaceExifToolVars( if current.attr == k { value = fmt.Sprintf("%v", v) // replace forward and backward slashes with underscore + // TODO: Make this configurable? value = strings.ReplaceAll(value, `/`, "_") value = strings.ReplaceAll(value, `\`, "_")