Metadata: Skip brute-force search if no Exif headers were found #2196
This commit is contained in:
parent
50ae86aeb5
commit
39b0530313
15 changed files with 161 additions and 104 deletions
9
Makefile
9
Makefile
|
@ -50,6 +50,9 @@ fmt: fmt-js fmt-go
|
||||||
clean-local: clean-local-config clean-local-cache
|
clean-local: clean-local-config clean-local-cache
|
||||||
upgrade: dep-upgrade-js dep-upgrade
|
upgrade: dep-upgrade-js dep-upgrade
|
||||||
devtools: install-go dep-npm
|
devtools: install-go dep-npm
|
||||||
|
.SILENT: help;
|
||||||
|
help:
|
||||||
|
@echo "For build instructions, visit <https://docs.photoprism.app/developer-guide/>."
|
||||||
fix-permissions:
|
fix-permissions:
|
||||||
$(info Updating filesystem permissions...)
|
$(info Updating filesystem permissions...)
|
||||||
@if [ $(UID) != 0 ]; then\
|
@if [ $(UID) != 0 ]; then\
|
||||||
|
@ -405,7 +408,11 @@ fmt-go:
|
||||||
goimports -w pkg internal cmd
|
goimports -w pkg internal cmd
|
||||||
tidy:
|
tidy:
|
||||||
go mod tidy -go=1.16 && go mod tidy -go=1.17
|
go mod tidy -go=1.16 && go mod tidy -go=1.17
|
||||||
|
|
||||||
.PHONY: all build dev dep-npm dep dep-go dep-js dep-list dep-tensorflow dep-upgrade dep-upgrade-js test test-js test-go \
|
.PHONY: all build dev dep-npm dep dep-go dep-js dep-list dep-tensorflow dep-upgrade dep-upgrade-js test test-js test-go \
|
||||||
install generate fmt fmt-go fmt-js upgrade start stop terminal root-terminal packer-digitalocean acceptance clean tidy \
|
install generate fmt fmt-go fmt-js upgrade start stop terminal root-terminal packer-digitalocean acceptance clean tidy \
|
||||||
docker-develop docker-preview docker-preview-all docker-preview-arm docker-release docker-release-all docker-release-arm \
|
docker-develop docker-preview docker-preview-all docker-preview-arm docker-release docker-release-all docker-release-arm \
|
||||||
install-go install-darktable install-tensorflow devtools tar.gz fix-permissions rootshell;
|
install-go install-darktable install-tensorflow devtools tar.gz fix-permissions rootshell help \
|
||||||
|
docker-local docker-local-all docker-local-bookworm docker-local-bullseye docker-local-buster docker-local-impish \
|
||||||
|
docker-local-develop docker-local-develop-all docker-local-develop-bookworm docker-local-develop-bullseye \
|
||||||
|
docker-local-develop-buster docker-local-develop-impish;
|
||||||
|
|
|
@ -63,11 +63,10 @@ func configAction(ctx *cli.Context) error {
|
||||||
fmt.Printf("%-25s %d\n", "auto-index", conf.AutoIndex()/time.Second)
|
fmt.Printf("%-25s %d\n", "auto-index", conf.AutoIndex()/time.Second)
|
||||||
fmt.Printf("%-25s %d\n", "auto-import", conf.AutoImport()/time.Second)
|
fmt.Printf("%-25s %d\n", "auto-import", conf.AutoImport()/time.Second)
|
||||||
|
|
||||||
// Features.
|
// Feature Flags.
|
||||||
fmt.Printf("%-25s %t\n", "disable-backups", conf.DisableBackups())
|
fmt.Printf("%-25s %t\n", "disable-backups", conf.DisableBackups())
|
||||||
fmt.Printf("%-25s %t\n", "disable-settings", conf.DisableSettings())
|
fmt.Printf("%-25s %t\n", "disable-settings", conf.DisableSettings())
|
||||||
fmt.Printf("%-25s %t\n", "disable-places", conf.DisablePlaces())
|
fmt.Printf("%-25s %t\n", "disable-places", conf.DisablePlaces())
|
||||||
fmt.Printf("%-25s %t\n", "disable-exiftool", conf.DisableExifTool())
|
|
||||||
fmt.Printf("%-25s %t\n", "disable-tensorflow", conf.DisableTensorFlow())
|
fmt.Printf("%-25s %t\n", "disable-tensorflow", conf.DisableTensorFlow())
|
||||||
fmt.Printf("%-25s %t\n", "disable-faces", conf.DisableFaces())
|
fmt.Printf("%-25s %t\n", "disable-faces", conf.DisableFaces())
|
||||||
fmt.Printf("%-25s %t\n", "disable-classification", conf.DisableClassification())
|
fmt.Printf("%-25s %t\n", "disable-classification", conf.DisableClassification())
|
||||||
|
@ -76,12 +75,17 @@ func configAction(ctx *cli.Context) error {
|
||||||
fmt.Printf("%-25s %t\n", "disable-sips", conf.DisableSips())
|
fmt.Printf("%-25s %t\n", "disable-sips", conf.DisableSips())
|
||||||
fmt.Printf("%-25s %t\n", "disable-heifconvert", conf.DisableHeifConvert())
|
fmt.Printf("%-25s %t\n", "disable-heifconvert", conf.DisableHeifConvert())
|
||||||
fmt.Printf("%-25s %t\n", "disable-ffmpeg", conf.DisableFFmpeg())
|
fmt.Printf("%-25s %t\n", "disable-ffmpeg", conf.DisableFFmpeg())
|
||||||
|
fmt.Printf("%-25s %t\n", "disable-exiftool", conf.DisableExifTool())
|
||||||
|
|
||||||
|
// Format Flags.
|
||||||
|
fmt.Printf("%-25s %t\n", "exif-bruteforce", conf.ExifBruteForce())
|
||||||
|
fmt.Printf("%-25s %t\n", "raw-presets", conf.RawPresets())
|
||||||
|
|
||||||
// TensorFlow.
|
// TensorFlow.
|
||||||
fmt.Printf("%-25s %s\n", "tensorflow-version", conf.TensorFlowVersion())
|
|
||||||
fmt.Printf("%-25s %s\n", "tensorflow-model-path", conf.TensorFlowModelPath())
|
|
||||||
fmt.Printf("%-25s %t\n", "detect-nsfw", conf.DetectNSFW())
|
fmt.Printf("%-25s %t\n", "detect-nsfw", conf.DetectNSFW())
|
||||||
fmt.Printf("%-25s %t\n", "upload-nsfw", conf.UploadNSFW())
|
fmt.Printf("%-25s %t\n", "upload-nsfw", conf.UploadNSFW())
|
||||||
|
fmt.Printf("%-25s %s\n", "tensorflow-version", conf.TensorFlowVersion())
|
||||||
|
fmt.Printf("%-25s %s\n", "tensorflow-model-path", conf.TensorFlowModelPath())
|
||||||
|
|
||||||
// UI Defaults.
|
// UI Defaults.
|
||||||
fmt.Printf("%-25s %s\n", "default-locale", conf.DefaultLocale())
|
fmt.Printf("%-25s %s\n", "default-locale", conf.DefaultLocale())
|
||||||
|
@ -110,7 +114,7 @@ func configAction(ctx *cli.Context) error {
|
||||||
fmt.Printf("%-25s %s\n", "api-uri", conf.ApiUri())
|
fmt.Printf("%-25s %s\n", "api-uri", conf.ApiUri())
|
||||||
fmt.Printf("%-25s %s\n", "base-uri", conf.BaseUri("/"))
|
fmt.Printf("%-25s %s\n", "base-uri", conf.BaseUri("/"))
|
||||||
|
|
||||||
// Web Server..
|
// Web Server.
|
||||||
fmt.Printf("%-25s %s\n", "http-host", conf.HttpHost())
|
fmt.Printf("%-25s %s\n", "http-host", conf.HttpHost())
|
||||||
fmt.Printf("%-25s %d\n", "http-port", conf.HttpPort())
|
fmt.Printf("%-25s %d\n", "http-port", conf.HttpPort())
|
||||||
fmt.Printf("%-25s %s\n", "http-mode", conf.HttpMode())
|
fmt.Printf("%-25s %s\n", "http-mode", conf.HttpMode())
|
||||||
|
@ -127,7 +131,6 @@ func configAction(ctx *cli.Context) error {
|
||||||
fmt.Printf("%-25s %d\n", "database-conns-idle", conf.DatabaseConnsIdle())
|
fmt.Printf("%-25s %d\n", "database-conns-idle", conf.DatabaseConnsIdle())
|
||||||
|
|
||||||
// External Tools.
|
// External Tools.
|
||||||
fmt.Printf("%-25s %t\n", "raw-presets", conf.RawPresets())
|
|
||||||
fmt.Printf("%-25s %s\n", "darktable-bin", conf.DarktableBin())
|
fmt.Printf("%-25s %s\n", "darktable-bin", conf.DarktableBin())
|
||||||
fmt.Printf("%-25s %s\n", "darktable-cache-path", conf.DarktableCachePath())
|
fmt.Printf("%-25s %s\n", "darktable-cache-path", conf.DarktableCachePath())
|
||||||
fmt.Printf("%-25s %s\n", "darktable-config-path", conf.DarktableConfigPath())
|
fmt.Printf("%-25s %s\n", "darktable-config-path", conf.DarktableConfigPath())
|
||||||
|
|
|
@ -148,13 +148,6 @@ func TestConfig_ImportPath(t *testing.T) {
|
||||||
assert.True(t, strings.HasSuffix(result, "/storage/testdata/import"))
|
assert.True(t, strings.HasSuffix(result, "/storage/testdata/import"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_ExifToolBin(t *testing.T) {
|
|
||||||
c := NewConfig(CliTestContext())
|
|
||||||
|
|
||||||
bin := c.ExifToolBin()
|
|
||||||
assert.Equal(t, "/usr/bin/exiftool", bin)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_CachePath(t *testing.T) {
|
func TestConfig_CachePath(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
|
|
@ -170,16 +170,6 @@ var GlobalFlags = []cli.Flag{
|
||||||
Usage: "disable creating YAML metadata files",
|
Usage: "disable creating YAML metadata files",
|
||||||
EnvVar: "PHOTOPRISM_DISABLE_BACKUPS",
|
EnvVar: "PHOTOPRISM_DISABLE_BACKUPS",
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "disable-exiftool",
|
|
||||||
Usage: "disable creating JSON metadata sidecar files with ExifTool",
|
|
||||||
EnvVar: "PHOTOPRISM_DISABLE_EXIFTOOL",
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "disable-ffmpeg",
|
|
||||||
Usage: "disable video transcoding and thumbnail extraction with FFmpeg",
|
|
||||||
EnvVar: "PHOTOPRISM_DISABLE_FFMPEG",
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "disable-darktable",
|
Name: "disable-darktable",
|
||||||
Usage: "disable converting RAW files with Darktable",
|
Usage: "disable converting RAW files with Darktable",
|
||||||
|
@ -215,6 +205,26 @@ var GlobalFlags = []cli.Flag{
|
||||||
Usage: "disable image classification",
|
Usage: "disable image classification",
|
||||||
EnvVar: "PHOTOPRISM_DISABLE_CLASSIFICATION",
|
EnvVar: "PHOTOPRISM_DISABLE_CLASSIFICATION",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "disable-ffmpeg",
|
||||||
|
Usage: "disable video transcoding and thumbnail extraction with FFmpeg",
|
||||||
|
EnvVar: "PHOTOPRISM_DISABLE_FFMPEG",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "disable-exiftool",
|
||||||
|
Usage: "disable creating JSON metadata sidecar files with ExifTool",
|
||||||
|
EnvVar: "PHOTOPRISM_DISABLE_EXIFTOOL",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "exif-bruteforce",
|
||||||
|
Usage: "always perform a brute-force search if no Exif headers were found",
|
||||||
|
EnvVar: "PHOTOPRISM_EXIF_BRUTEFORCE",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "raw-presets",
|
||||||
|
Usage: "enable RAW file converter presets (may reduce performance)",
|
||||||
|
EnvVar: "PHOTOPRISM_RAW_PRESETS",
|
||||||
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "detect-nsfw",
|
Name: "detect-nsfw",
|
||||||
Usage: "flag photos as private that may be offensive (requires TensorFlow)",
|
Usage: "flag photos as private that may be offensive (requires TensorFlow)",
|
||||||
|
@ -368,11 +378,6 @@ var GlobalFlags = []cli.Flag{
|
||||||
Usage: "maximum `NUMBER` of idle database connections",
|
Usage: "maximum `NUMBER` of idle database connections",
|
||||||
EnvVar: "PHOTOPRISM_DATABASE_CONNS_IDLE",
|
EnvVar: "PHOTOPRISM_DATABASE_CONNS_IDLE",
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "raw-presets",
|
|
||||||
Usage: "enable RAW file converter presets (may reduce performance)",
|
|
||||||
EnvVar: "PHOTOPRISM_RAW_PRESETS",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "darktable-bin",
|
Name: "darktable-bin",
|
||||||
Usage: "Darktable CLI `COMMAND` for RAW image conversion",
|
Usage: "Darktable CLI `COMMAND` for RAW image conversion",
|
||||||
|
|
|
@ -216,21 +216,6 @@ func (c *Config) ImportPath() string {
|
||||||
return fs.Abs(c.options.ImportPath)
|
return fs.Abs(c.options.ImportPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExifToolBin returns the exiftool executable file name.
|
|
||||||
func (c *Config) ExifToolBin() string {
|
|
||||||
return findExecutable(c.options.ExifToolBin, "exiftool")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExifToolJson tests if creating JSON metadata sidecar files with Exiftool is enabled.
|
|
||||||
func (c *Config) ExifToolJson() bool {
|
|
||||||
return !c.DisableExifTool()
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackupYaml tests if creating YAML files is enabled.
|
|
||||||
func (c *Config) BackupYaml() bool {
|
|
||||||
return !c.DisableBackups()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SidecarPath returns the storage path for generated sidecar files (relative or absolute).
|
// SidecarPath returns the storage path for generated sidecar files (relative or absolute).
|
||||||
func (c *Config) SidecarPath() string {
|
func (c *Config) SidecarPath() string {
|
||||||
if c.options.SidecarPath == "" {
|
if c.options.SidecarPath == "" {
|
||||||
|
|
|
@ -11,30 +11,6 @@ func TestConfig_FindExecutable(t *testing.T) {
|
||||||
assert.Equal(t, "", findExecutable("yyy", "xxx"))
|
assert.Equal(t, "", findExecutable("yyy", "xxx"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_SidecarJson(t *testing.T) {
|
|
||||||
c := NewConfig(CliTestContext())
|
|
||||||
|
|
||||||
assert.Equal(t, true, c.ExifToolJson())
|
|
||||||
assert.Equal(t, c.DisableExifTool(), !c.ExifToolJson())
|
|
||||||
|
|
||||||
c.options.DisableExifTool = true
|
|
||||||
|
|
||||||
assert.Equal(t, false, c.ExifToolJson())
|
|
||||||
assert.Equal(t, c.DisableExifTool(), !c.ExifToolJson())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_SidecarYaml(t *testing.T) {
|
|
||||||
c := NewConfig(CliTestContext())
|
|
||||||
|
|
||||||
assert.Equal(t, true, c.BackupYaml())
|
|
||||||
assert.Equal(t, c.DisableBackups(), !c.BackupYaml())
|
|
||||||
|
|
||||||
c.options.DisableBackups = true
|
|
||||||
|
|
||||||
assert.Equal(t, false, c.BackupYaml())
|
|
||||||
assert.Equal(t, c.DisableBackups(), !c.BackupYaml())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_SidecarPath(t *testing.T) {
|
func TestConfig_SidecarPath(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
|
21
internal/config/metadata.go
Normal file
21
internal/config/metadata.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
// ExifBruteForce checks if a brute-force search should be performed when no Exif headers were found.
|
||||||
|
func (c *Config) ExifBruteForce() bool {
|
||||||
|
return c.options.ExifBruteForce || !c.ExifToolJson()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExifToolBin returns the exiftool executable file name.
|
||||||
|
func (c *Config) ExifToolBin() string {
|
||||||
|
return findExecutable(c.options.ExifToolBin, "exiftool")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExifToolJson tests if creating JSON metadata sidecar files with Exiftool is enabled.
|
||||||
|
func (c *Config) ExifToolJson() bool {
|
||||||
|
return !c.DisableExifTool()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackupYaml tests if creating YAML files is enabled.
|
||||||
|
func (c *Config) BackupYaml() bool {
|
||||||
|
return !c.DisableBackups()
|
||||||
|
}
|
43
internal/config/metadata_test.go
Normal file
43
internal/config/metadata_test.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_ExifBruteForce(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
assert.Equal(t, false, c.ExifBruteForce())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_ExifToolBin(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
bin := c.ExifToolBin()
|
||||||
|
assert.Equal(t, "/usr/bin/exiftool", bin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_ExifToolJson(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
assert.Equal(t, true, c.ExifToolJson())
|
||||||
|
assert.Equal(t, c.DisableExifTool(), !c.ExifToolJson())
|
||||||
|
|
||||||
|
c.options.DisableExifTool = true
|
||||||
|
|
||||||
|
assert.Equal(t, false, c.ExifToolJson())
|
||||||
|
assert.Equal(t, c.DisableExifTool(), !c.ExifToolJson())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_SidecarYaml(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
assert.Equal(t, true, c.BackupYaml())
|
||||||
|
assert.Equal(t, c.DisableBackups(), !c.BackupYaml())
|
||||||
|
|
||||||
|
c.options.DisableBackups = true
|
||||||
|
|
||||||
|
assert.Equal(t, false, c.BackupYaml())
|
||||||
|
assert.Equal(t, c.DisableBackups(), !c.BackupYaml())
|
||||||
|
}
|
|
@ -65,8 +65,6 @@ type Options struct {
|
||||||
DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"`
|
DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"`
|
||||||
DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"`
|
DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"`
|
||||||
DisablePlaces bool `yaml:"DisablePlaces" json:"DisablePlaces" flag:"disable-places"`
|
DisablePlaces bool `yaml:"DisablePlaces" json:"DisablePlaces" flag:"disable-places"`
|
||||||
DisableExifTool bool `yaml:"DisableExifTool" json:"DisableExifTool" flag:"disable-exiftool"`
|
|
||||||
DisableFFmpeg bool `yaml:"DisableFFmpeg" json:"DisableFFmpeg" flag:"disable-ffmpeg"`
|
|
||||||
DisableDarktable bool `yaml:"DisableDarktable" json:"DisableDarktable" flag:"disable-darktable"`
|
DisableDarktable bool `yaml:"DisableDarktable" json:"DisableDarktable" flag:"disable-darktable"`
|
||||||
DisableRawtherapee bool `yaml:"DisableRawtherapee" json:"DisableRawtherapee" flag:"disable-rawtherapee"`
|
DisableRawtherapee bool `yaml:"DisableRawtherapee" json:"DisableRawtherapee" flag:"disable-rawtherapee"`
|
||||||
DisableSips bool `yaml:"DisableSips" json:"DisableSips" flag:"disable-sips"`
|
DisableSips bool `yaml:"DisableSips" json:"DisableSips" flag:"disable-sips"`
|
||||||
|
@ -74,6 +72,10 @@ type Options struct {
|
||||||
DisableTensorFlow bool `yaml:"DisableTensorFlow" json:"DisableTensorFlow" flag:"disable-tensorflow"`
|
DisableTensorFlow bool `yaml:"DisableTensorFlow" json:"DisableTensorFlow" flag:"disable-tensorflow"`
|
||||||
DisableFaces bool `yaml:"DisableFaces" json:"DisableFaces" flag:"disable-faces"`
|
DisableFaces bool `yaml:"DisableFaces" json:"DisableFaces" flag:"disable-faces"`
|
||||||
DisableClassification bool `yaml:"DisableClassification" json:"DisableClassification" flag:"disable-classification"`
|
DisableClassification bool `yaml:"DisableClassification" json:"DisableClassification" flag:"disable-classification"`
|
||||||
|
DisableFFmpeg bool `yaml:"DisableFFmpeg" json:"DisableFFmpeg" flag:"disable-ffmpeg"`
|
||||||
|
DisableExifTool bool `yaml:"DisableExifTool" json:"DisableExifTool" flag:"disable-exiftool"`
|
||||||
|
ExifBruteForce bool `yaml:"ExifBruteForce" json:"ExifBruteForce" flag:"exif-bruteforce"`
|
||||||
|
RawPresets bool `yaml:"RawPresets" json:"RawPresets" flag:"raw-presets"`
|
||||||
DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"`
|
DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"`
|
||||||
UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"`
|
UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"`
|
||||||
DefaultTheme string `yaml:"DefaultTheme" json:"DefaultTheme" flag:"default-theme"`
|
DefaultTheme string `yaml:"DefaultTheme" json:"DefaultTheme" flag:"default-theme"`
|
||||||
|
@ -102,7 +104,6 @@ type Options struct {
|
||||||
HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"`
|
HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"`
|
||||||
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
||||||
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
||||||
RawPresets bool `yaml:"RawPresets" json:"RawPresets" flag:"raw-presets"`
|
|
||||||
DarktableBin string `yaml:"DarktableBin" json:"-" flag:"darktable-bin"`
|
DarktableBin string `yaml:"DarktableBin" json:"-" flag:"darktable-bin"`
|
||||||
DarktableBlacklist string `yaml:"DarktableBlacklist" json:"-" flag:"darktable-blacklist"`
|
DarktableBlacklist string `yaml:"DarktableBlacklist" json:"-" flag:"darktable-blacklist"`
|
||||||
RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"`
|
RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"`
|
||||||
|
|
|
@ -63,6 +63,7 @@ func NewTestOptions() *Options {
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
DetectNSFW: true,
|
DetectNSFW: true,
|
||||||
UploadNSFW: false,
|
UploadNSFW: false,
|
||||||
|
ExifBruteForce: false,
|
||||||
AssetsPath: assetsPath,
|
AssetsPath: assetsPath,
|
||||||
AutoIndex: -1,
|
AutoIndex: -1,
|
||||||
AutoImport: 7200,
|
AutoImport: 7200,
|
||||||
|
|
|
@ -12,7 +12,7 @@ const (
|
||||||
ImageTypeHDR = 3 // see https://exiftool.org/TagNames/Apple.html
|
ImageTypeHDR = 3 // see https://exiftool.org/TagNames/Apple.html
|
||||||
)
|
)
|
||||||
|
|
||||||
// Data represents image meta data.
|
// Data represents image metadata.
|
||||||
type Data struct {
|
type Data struct {
|
||||||
FileName string `meta:"FileName"`
|
FileName string `meta:"FileName"`
|
||||||
DocumentID string `meta:"BurstUUID,MediaGroupUUID,ImageUniqueID,OriginalDocumentID,DocumentID"`
|
DocumentID string `meta:"BurstUUID,MediaGroupUUID,ImageUniqueID,OriginalDocumentID,DocumentID"`
|
||||||
|
|
|
@ -34,15 +34,15 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exif parses an image file for Exif meta data and returns as Data struct.
|
// Exif parses an image file for Exif metadata and returns as Data struct.
|
||||||
func Exif(fileName string, fileType fs.FileFormat) (data Data, err error) {
|
func Exif(fileName string, fileType fs.FileFormat, bruteForce bool) (data Data, err error) {
|
||||||
err = data.Exif(fileName, fileType)
|
err = data.Exif(fileName, fileType, bruteForce)
|
||||||
|
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exif parses an image file for Exif meta data and returns as Data struct.
|
// Exif parses an image file for Exif metadata and returns as Data struct.
|
||||||
func (data *Data) Exif(fileName string, fileType fs.FileFormat) (err error) {
|
func (data *Data) Exif(fileName string, fileType fs.FileFormat, bruteForce bool) (err error) {
|
||||||
exifMutex.Lock()
|
exifMutex.Lock()
|
||||||
defer exifMutex.Unlock()
|
defer exifMutex.Unlock()
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ func (data *Data) Exif(fileName string, fileType fs.FileFormat) (err error) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Extract raw Exif block.
|
// Extract raw Exif block.
|
||||||
rawExif, err := RawExif(fileName, fileType)
|
rawExif, err := RawExif(fileName, fileType, bruteForce)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error) {
|
func RawExif(fileName string, fileType fs.FileFormat, bruteForce bool) (rawExif []byte, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if e := recover(); e != nil {
|
if e := recover(); e != nil {
|
||||||
err = fmt.Errorf("metadata: %s in %s (raw exif panic)\nstack: %s", e, sanitize.Log(filepath.Base(fileName)), debug.Stack())
|
err = fmt.Errorf("metadata: %s in %s (raw exif panic)\nstack: %s", e, sanitize.Log(filepath.Base(fileName)), debug.Stack())
|
||||||
|
@ -25,8 +25,10 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
|
||||||
// Extract raw Exif block.
|
// Extract raw Exif block.
|
||||||
var parsed bool
|
var parsed bool
|
||||||
|
|
||||||
|
// Sanitized and shortened file name for logs.
|
||||||
logName := sanitize.Log(filepath.Base(fileName))
|
logName := sanitize.Log(filepath.Base(fileName))
|
||||||
|
|
||||||
|
// Try Exif parser for specific media file format first.
|
||||||
if fileType == fs.FormatJpeg {
|
if fileType == fs.FormatJpeg {
|
||||||
jpegMp := jpegstructure.NewJpegMediaParser()
|
jpegMp := jpegstructure.NewJpegMediaParser()
|
||||||
|
|
||||||
|
@ -38,7 +40,7 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
|
||||||
_, rawExif, err = sl.Exif()
|
_, rawExif, err = sl.Exif()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.HasPrefix(err.Error(), "no exif header") {
|
if !bruteForce || strings.HasPrefix(err.Error(), "no exif header") {
|
||||||
return rawExif, fmt.Errorf("metadata: found no exif header in %s (parse jpeg)", logName)
|
return rawExif, fmt.Errorf("metadata: found no exif header in %s (parse jpeg)", logName)
|
||||||
} else if strings.HasPrefix(err.Error(), "no exif data") {
|
} else if strings.HasPrefix(err.Error(), "no exif data") {
|
||||||
log.Debugf("metadata: failed parsing %s, starting brute-force search (parse jpeg)", logName)
|
log.Debugf("metadata: failed parsing %s, starting brute-force search (parse jpeg)", logName)
|
||||||
|
@ -109,9 +111,13 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
|
||||||
parsed = true
|
parsed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("metadata: no file format parser for %s, performing brute-force search", logName)
|
||||||
|
bruteForce = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !parsed {
|
// Start brute-force search for Exif data?
|
||||||
|
if !parsed && bruteForce {
|
||||||
rawExif, err = exif.SearchFileAndExtractExif(fileName)
|
rawExif, err = exif.SearchFileAndExtractExif(fileName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
func TestExif(t *testing.T) {
|
func TestExif(t *testing.T) {
|
||||||
t.Run("photoshop.jpg", func(t *testing.T) {
|
t.Run("photoshop.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/photoshop.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/photoshop.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -40,7 +40,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ladybug.jpg", func(t *testing.T) {
|
t.Run("ladybug.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/ladybug.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/ladybug.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -72,7 +72,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("gopro_hd2.jpg", func(t *testing.T) {
|
t.Run("gopro_hd2.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/gopro_hd2.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/gopro_hd2.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -102,7 +102,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("tweethog.png", func(t *testing.T) {
|
t.Run("tweethog.png", func(t *testing.T) {
|
||||||
_, err := Exif("testdata/tweethog.png", fs.FormatPng)
|
_, err := Exif("testdata/tweethog.png", fs.FormatPng, true)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("err should NOT be nil")
|
t.Fatal("err should NOT be nil")
|
||||||
|
@ -112,7 +112,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/iphone_7.heic", fs.FormatHEIF)
|
data, err := Exif("testdata/iphone_7.heic", fs.FormatHEIF, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -133,7 +133,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("gps-2000.jpg", func(t *testing.T) {
|
t.Run("gps-2000.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/gps-2000.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/gps-2000.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -161,7 +161,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("image-2011.jpg", func(t *testing.T) {
|
t.Run("image-2011.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/image-2011.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/image-2011.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -196,7 +196,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ship.jpg", func(t *testing.T) {
|
t.Run("ship.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/ship.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/ship.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -217,7 +217,17 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("no-exif-data.jpg", func(t *testing.T) {
|
t.Run("no-exif-data.jpg", func(t *testing.T) {
|
||||||
_, err := Exif("testdata/no-exif-data.jpg", fs.FormatJpeg)
|
_, err := Exif("testdata/no-exif-data.jpg", fs.FormatJpeg, false)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("err should NOT be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "metadata: found no exif header in no-exif-data.jpg (parse jpeg)", err.Error())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no-exif-data.jpg/BruteForce", func(t *testing.T) {
|
||||||
|
_, err := Exif("testdata/no-exif-data.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("err should NOT be nil")
|
t.Fatal("err should NOT be nil")
|
||||||
|
@ -227,7 +237,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("screenshot.png", func(t *testing.T) {
|
t.Run("screenshot.png", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/screenshot.png", fs.FormatPng)
|
data, err := Exif("testdata/screenshot.png", fs.FormatPng, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -238,7 +248,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("orientation.jpg", func(t *testing.T) {
|
t.Run("orientation.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/orientation.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/orientation.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -266,13 +276,19 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("gopher-preview.jpg", func(t *testing.T) {
|
t.Run("gopher-preview.jpg", func(t *testing.T) {
|
||||||
_, err := Exif("testdata/gopher-preview.jpg", fs.FormatJpeg)
|
_, err := Exif("testdata/gopher-preview.jpg", fs.FormatJpeg, false)
|
||||||
|
|
||||||
|
assert.EqualError(t, err, "metadata: found no exif header in gopher-preview.jpg (parse jpeg)")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gopher-preview.jpg/BruteForce", func(t *testing.T) {
|
||||||
|
_, err := Exif("testdata/gopher-preview.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
assert.EqualError(t, err, "metadata: found no exif header in gopher-preview.jpg (search and extract)")
|
assert.EqualError(t, err, "metadata: found no exif header in gopher-preview.jpg (search and extract)")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("huawei-gps-error.jpg", func(t *testing.T) {
|
t.Run("huawei-gps-error.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/huawei-gps-error.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/huawei-gps-error.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -293,7 +309,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("panorama360.jpg", func(t *testing.T) {
|
t.Run("panorama360.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/panorama360.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/panorama360.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -325,7 +341,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("exif-example.tiff", func(t *testing.T) {
|
t.Run("exif-example.tiff", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/exif-example.tiff", fs.FormatTiff)
|
data, err := Exif("testdata/exif-example.tiff", fs.FormatTiff, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -357,7 +373,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("out-of-range-500.jpg", func(t *testing.T) {
|
t.Run("out-of-range-500.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/out-of-range-500.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/out-of-range-500.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -389,7 +405,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("digikam.jpg", func(t *testing.T) {
|
t.Run("digikam.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/digikam.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/digikam.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -424,7 +440,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("notebook.jpg", func(t *testing.T) {
|
t.Run("notebook.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/notebook.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/notebook.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -445,7 +461,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("snow.jpg", func(t *testing.T) {
|
t.Run("snow.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/snow.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/snow.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -466,7 +482,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("keywords.jpg", func(t *testing.T) {
|
t.Run("keywords.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/keywords.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/keywords.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -486,7 +502,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Iceland-P3.jpg", func(t *testing.T) {
|
t.Run("Iceland-P3.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/Iceland-P3.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/Iceland-P3.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -518,7 +534,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Iceland-sRGB.jpg", func(t *testing.T) {
|
t.Run("Iceland-sRGB.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/Iceland-sRGB.jpg", fs.FormatJpeg)
|
data, err := Exif("testdata/Iceland-sRGB.jpg", fs.FormatJpeg, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
@ -72,7 +72,7 @@ func (m *MediaFile) MetaData() (result meta.Data) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if m.ExifSupported() {
|
if m.ExifSupported() {
|
||||||
err = m.metaData.Exif(m.FileName(), m.FileType())
|
err = m.metaData.Exif(m.FileName(), m.FileType(), Config().ExifBruteForce())
|
||||||
} else {
|
} else {
|
||||||
err = fmt.Errorf("exif not supported")
|
err = fmt.Errorf("exif not supported")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue