diff --git a/internal/api/covers.go b/internal/api/covers.go index d7230a04a..b60cdbdc3 100644 --- a/internal/api/covers.go +++ b/internal/api/covers.go @@ -105,9 +105,9 @@ func AlbumCover(router *gin.RouterGroup) { var thumbnail string if conf.ThumbUncached() || size.Uncached() { - thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), size.Width, size.Height, f.FileOrientation, size.Options...) + thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, f.FileOrientation, size.Options...) } else { - thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), size.Width, size.Height, size.Options...) + thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, size.Options...) } if err != nil { @@ -219,9 +219,9 @@ func LabelCover(router *gin.RouterGroup) { var thumbnail string if conf.ThumbUncached() || size.Uncached() { - thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), size.Width, size.Height, f.FileOrientation, size.Options...) + thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, f.FileOrientation, size.Options...) } else { - thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), size.Width, size.Height, size.Options...) + thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, size.Options...) } if err != nil { diff --git a/internal/api/folder_cover.go b/internal/api/folder_cover.go index 266ee1171..412bf6a1c 100644 --- a/internal/api/folder_cover.go +++ b/internal/api/folder_cover.go @@ -114,9 +114,9 @@ func FolderCover(router *gin.RouterGroup) { var thumbnail string if conf.ThumbUncached() || size.Uncached() { - thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), size.Width, size.Height, f.FileOrientation, size.Options...) + thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, f.FileOrientation, size.Options...) } else { - thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), size.Width, size.Height, size.Options...) + thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, size.Options...) } if err != nil { diff --git a/internal/api/share_preview.go b/internal/api/share_preview.go index 40bb968e4..c7fb6cc5a 100644 --- a/internal/api/share_preview.go +++ b/internal/api/share_preview.go @@ -40,7 +40,7 @@ func SharePreview(router *gin.RouterGroup) { return } - thumbPath := path.Join(conf.ThumbPath(), "share") + thumbPath := path.Join(conf.ThumbCachePath(), "share") if err := os.MkdirAll(thumbPath, os.ModePerm); err != nil { log.Error(err) @@ -101,7 +101,7 @@ func SharePreview(router *gin.RouterGroup) { return } - thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), size.Width, size.Height, f.FileOrientation, size.Options...) + thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, f.FileOrientation, size.Options...) if err != nil { log.Error(err) @@ -131,7 +131,7 @@ func SharePreview(router *gin.RouterGroup) { return } - thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), size.Width, size.Height, f.FileOrientation, size.Options...) + thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, f.FileOrientation, size.Options...) if err != nil { log.Error(err) diff --git a/internal/api/thumbnails.go b/internal/api/thumbnails.go index e6b234608..bf16a2640 100644 --- a/internal/api/thumbnails.go +++ b/internal/api/thumbnails.go @@ -51,7 +51,7 @@ func GetThumb(router *gin.RouterGroup) { return } - fileName, err := crop.FromRequest(fileHash, cropArea, cropSize, conf.ThumbPath()) + fileName, err := crop.FromRequest(fileHash, cropArea, cropSize, conf.ThumbCachePath()) if err != nil { log.Warnf("%s: %s", logPrefix, err) @@ -119,7 +119,7 @@ func GetThumb(router *gin.RouterGroup) { // Return existing thumbs straight away. if !download { - if fileName, err := thumb.FileName(fileHash, conf.ThumbPath(), size.Width, size.Height, size.Options...); err == nil && fs.FileExists(fileName) { + if fileName, err := thumb.FileName(fileHash, conf.ThumbCachePath(), size.Width, size.Height, size.Options...); err == nil && fs.FileExists(fileName) { AddThumbCacheHeader(c) c.File(fileName) return @@ -183,9 +183,9 @@ func GetThumb(router *gin.RouterGroup) { var thumbnail string if conf.ThumbUncached() || size.Uncached() { - thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), size.Width, size.Height, f.FileOrientation, size.Options...) + thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, f.FileOrientation, size.Options...) } else { - thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), size.Width, size.Height, size.Options...) + thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, size.Options...) } if err != nil { diff --git a/internal/commands/thumbs.go b/internal/commands/thumbs.go index 500d4f731..9bc28dc73 100644 --- a/internal/commands/thumbs.go +++ b/internal/commands/thumbs.go @@ -34,7 +34,7 @@ func thumbsAction(ctx *cli.Context) error { return err } - log.Infof("creating thumbnails in %s", clean.Log(conf.ThumbPath())) + log.Infof("creating thumbnails in %s", clean.Log(conf.ThumbCachePath())) rs := service.Resample() diff --git a/internal/config/config_filepaths.go b/internal/config/config_filepaths.go index afd358cf6..91945cf09 100644 --- a/internal/config/config_filepaths.go +++ b/internal/config/config_filepaths.go @@ -74,6 +74,12 @@ func (c *Config) CreateDirectories() error { return createError(c.StoragePath(), err) } + if c.CmdCachePath() == "" { + return notFoundError("cmd cache") + } else if err := os.MkdirAll(c.CmdCachePath(), os.ModePerm); err != nil { + return createError(c.CmdCachePath(), err) + } + if c.BackupPath() == "" { return notFoundError("backup") } else if err := os.MkdirAll(c.BackupPath(), os.ModePerm); err != nil { @@ -104,10 +110,10 @@ func (c *Config) CreateDirectories() error { return createError(c.CachePath(), err) } - if c.ThumbPath() == "" { + if c.ThumbCachePath() == "" { return notFoundError("thumbs") - } else if err := os.MkdirAll(c.ThumbPath(), os.ModePerm); err != nil { - return createError(c.ThumbPath(), err) + } else if err := os.MkdirAll(c.ThumbCachePath(), os.ModePerm); err != nil { + return createError(c.ThumbCachePath(), err) } if c.ConfigPath() == "" { @@ -153,12 +159,12 @@ func (c *Config) CreateDirectories() error { } if c.DarktableEnabled() { - if cachePath, err := c.CreateDarktableCachePath(); err != nil { - return fmt.Errorf("could not create darktable cache path %s", clean.Log(cachePath)) + if dir, err := c.CreateDarktableCachePath(); err != nil { + return fmt.Errorf("could not create darktable cache path %s", clean.Log(dir)) } - if configPath, err := c.CreateDarktableConfigPath(); err != nil { - return fmt.Errorf("could not create darktable cache path %s", clean.Log(configPath)) + if dir, err := c.CreateDarktableConfigPath(); err != nil { + return fmt.Errorf("could not create darktable cache path %s", clean.Log(dir)) } } @@ -263,11 +269,17 @@ func (c *Config) SidecarWritable() bool { // TempPath returns a temporary directory name for uploads and downloads. func (c *Config) TempPath() string { - if c.options.TempPath == "" { - return filepath.Join(os.TempDir(), "photoprism") + if c.options.TempPath != "" { + if c.options.TempPath[0] != '/' { + c.options.TempPath = fs.Abs(c.options.TempPath) + } + } else if dir, err := os.MkdirTemp(os.TempDir(), "photoprism"); err == nil { + c.options.TempPath = dir + } else { + c.options.TempPath = filepath.Join(os.TempDir(), "photoprism") } - return fs.Abs(c.options.TempPath) + return c.options.TempPath } // CachePath returns the path for cache files. @@ -279,6 +291,16 @@ func (c *Config) CachePath() string { return fs.Abs(c.options.CachePath) } +// CmdCachePath returns a path that CLI commands can use as cache directory. +func (c *Config) CmdCachePath() string { + return filepath.Join(c.CachePath(), "cmd") +} + +// ThumbCachePath returns the thumbnail storage directory. +func (c *Config) ThumbCachePath() string { + return c.CachePath() + "/thumbnails" +} + // StoragePath returns the path for generated files like cache and index. func (c *Config) StoragePath() string { if c.options.StoragePath == "" { diff --git a/internal/config/config_filepaths_test.go b/internal/config/config_filepaths_test.go index 0d79e265a..829ee25a8 100644 --- a/internal/config/config_filepaths_test.go +++ b/internal/config/config_filepaths_test.go @@ -1,6 +1,7 @@ package config import ( + "strings" "testing" "github.com/photoprism/photoprism/pkg/rnd" @@ -44,7 +45,21 @@ func TestConfig_TempPath(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, "/go/src/github.com/photoprism/photoprism/storage/testdata/temp", c.TempPath()) c.options.TempPath = "" - assert.Equal(t, "/tmp/photoprism", c.TempPath()) + + if dir := c.TempPath(); dir == "" { + t.Fatal("temp path is empty") + } else if !strings.HasPrefix(dir, "/tmp/photoprism") { + t.Fatalf("unexpected temp path: %s", dir) + } +} + +func TestConfig_CmdCachePath(t *testing.T) { + c := NewConfig(CliTestContext()) + if dir := c.CmdCachePath(); dir == "" { + t.Fatal("cmd cache path is empty") + } else if !strings.HasPrefix(dir, c.CachePath()) { + t.Fatalf("unexpected cmd cache path: %s", dir) + } } func TestConfig_CachePath2(t *testing.T) { diff --git a/internal/config/config_raw.go b/internal/config/config_raw.go index e7a6d910d..8abfe49f6 100644 --- a/internal/config/config_raw.go +++ b/internal/config/config_raw.go @@ -2,7 +2,8 @@ package config import ( "os" - "path/filepath" + + "github.com/photoprism/photoprism/pkg/fs" ) // RawEnabled checks if indexing and conversion of RAW files is enabled. @@ -27,27 +28,21 @@ func (c *Config) DarktableBlacklist() string { // DarktableConfigPath returns the darktable config directory. func (c *Config) DarktableConfigPath() string { - if c.options.DarktableConfigPath != "" { - return c.options.DarktableConfigPath - } - - return filepath.Join(c.ConfigPath(), "darktable") + return fs.Abs(c.options.DarktableConfigPath) } // DarktableCachePath returns the darktable cache directory. func (c *Config) DarktableCachePath() string { - if c.options.DarktableCachePath != "" { - return c.options.DarktableCachePath - } - - return filepath.Join(c.CachePath(), "darktable") + return fs.Abs(c.options.DarktableCachePath) } // CreateDarktableCachePath creates and returns the darktable cache directory. func (c *Config) CreateDarktableCachePath() (string, error) { cachePath := c.DarktableCachePath() - if err := os.MkdirAll(cachePath, os.ModePerm); err != nil { + if cachePath == "" { + return "", nil + } else if err := os.MkdirAll(cachePath, os.ModePerm); err != nil { return cachePath, err } else { c.options.DarktableCachePath = cachePath @@ -60,7 +55,9 @@ func (c *Config) CreateDarktableCachePath() (string, error) { func (c *Config) CreateDarktableConfigPath() (string, error) { configPath := c.DarktableConfigPath() - if err := os.MkdirAll(configPath, os.ModePerm); err != nil { + if configPath == "" { + return "", nil + } else if err := os.MkdirAll(configPath, os.ModePerm); err != nil { return configPath, err } else { c.options.DarktableConfigPath = configPath diff --git a/internal/config/config_report.go b/internal/config/config_report.go index 7ccd65865..33d6d389c 100644 --- a/internal/config/config_report.go +++ b/internal/config/config_report.go @@ -35,9 +35,11 @@ func (c *Config) Report() (rows [][]string, cols []string) { // Other paths. {"storage-path", c.StoragePath()}, {"sidecar-path", c.SidecarPath()}, - {"cache-path", c.CachePath()}, {"albums-path", c.AlbumsPath()}, {"backup-path", c.BackupPath()}, + {"cache-path", c.CachePath()}, + {"cmd-cache-path", c.CmdCachePath()}, + {"thumb-cache-path", c.ThumbCachePath()}, {"import-path", c.ImportPath()}, {"assets-path", c.AssetsPath()}, {"static-path", c.StaticPath()}, @@ -142,7 +144,6 @@ func (c *Config) Report() (rows [][]string, cols []string) { {"thumb-size", fmt.Sprintf("%d", c.ThumbSizePrecached())}, {"thumb-size-uncached", fmt.Sprintf("%d", c.ThumbSizeUncached())}, {"thumb-uncached", fmt.Sprintf("%t", c.ThumbUncached())}, - {"thumb-path", c.ThumbPath()}, {"jpeg-quality", fmt.Sprintf("%d", c.JpegQuality())}, {"jpeg-size", fmt.Sprintf("%d", c.JpegSize())}, diff --git a/internal/config/config_resample.go b/internal/config/config_resample.go index dee8540d4..18127f3a9 100644 --- a/internal/config/config_resample.go +++ b/internal/config/config_resample.go @@ -38,11 +38,6 @@ func (c *Config) ThumbFilter() thumb.ResampleFilter { } } -// ThumbPath returns the thumbnail storage directory. -func (c *Config) ThumbPath() string { - return c.CachePath() + "/thumbnails" -} - // ThumbColor returns the color profile name for thumbnails. func (c *Config) ThumbColor() string { return c.options.ThumbColor diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6c39162c2..1f8b1ff78 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -159,8 +159,8 @@ func TestConfig_CachePath(t *testing.T) { func TestConfig_ThumbnailsPath(t *testing.T) { c := NewConfig(CliTestContext()) - assert.True(t, strings.HasPrefix(c.ThumbPath(), "/")) - assert.True(t, strings.HasSuffix(c.ThumbPath(), "storage/testdata/cache/thumbnails")) + assert.True(t, strings.HasPrefix(c.ThumbCachePath(), "/")) + assert.True(t, strings.HasSuffix(c.ThumbCachePath(), "storage/testdata/cache/thumbnails")) } func TestConfig_AssetsPath(t *testing.T) { diff --git a/internal/config/global_flags.go b/internal/config/global_flags.go index e4fd6e515..1a45e794e 100644 --- a/internal/config/global_flags.go +++ b/internal/config/global_flags.go @@ -122,16 +122,16 @@ var GlobalFlags = []cli.Flag{ Usage: "custom relative or absolute sidecar `PATH` (optional)", EnvVar: "PHOTOPRISM_SIDECAR_PATH", }, - cli.StringFlag{ - Name: "cache-path", - Usage: "custom cache `PATH` for sessions and thumbnail files (optional)", - EnvVar: "PHOTOPRISM_CACHE_PATH", - }, cli.StringFlag{ Name: "backup-path", Usage: "custom backup `PATH` for index backup files (optional)", EnvVar: "PHOTOPRISM_BACKUP_PATH", }, + cli.StringFlag{ + Name: "cache-path", + Usage: "custom cache `PATH` for sessions and thumbnail files (optional)", + EnvVar: "PHOTOPRISM_CACHE_PATH", + }, cli.StringFlag{ Name: "import-path", Usage: "base `PATH` from which files can be imported to originals (optional)", @@ -418,13 +418,13 @@ var GlobalFlags = []cli.Flag{ }, cli.StringFlag{ Name: "darktable-cache-path", - Usage: "custom Darktable cache `PATH` (automatically created if empty)", + Usage: "custom Darktable cache `PATH`", Value: "", EnvVar: "PHOTOPRISM_DARKTABLE_CACHE_PATH", }, cli.StringFlag{ Name: "darktable-config-path", - Usage: "custom Darktable config `PATH` (automatically created if empty)", + Usage: "custom Darktable config `PATH`", Value: "", EnvVar: "PHOTOPRISM_DARKTABLE_CONFIG_PATH", }, diff --git a/internal/config/global_options.go b/internal/config/global_options.go index e4a07f42b..bd0d0ce97 100644 --- a/internal/config/global_options.go +++ b/internal/config/global_options.go @@ -42,8 +42,8 @@ type Options struct { ResolutionLimit int `yaml:"ResolutionLimit" json:"ResolutionLimit" flag:"resolution-limit"` StoragePath string `yaml:"StoragePath" json:"-" flag:"storage-path"` SidecarPath string `yaml:"SidecarPath" json:"-" flag:"sidecar-path"` - CachePath string `yaml:"CachePath" json:"-" flag:"cache-path"` BackupPath string `yaml:"BackupPath" json:"-" flag:"backup-path"` + CachePath string `yaml:"CachePath" json:"-" flag:"cache-path"` ImportPath string `yaml:"ImportPath" json:"-" flag:"import-path"` AssetsPath string `yaml:"AssetsPath" json:"-" flag:"assets-path"` TempPath string `yaml:"TempPath" json:"-" flag:"temp-path"` diff --git a/internal/photoprism/cleanup.go b/internal/photoprism/cleanup.go index 8240ce261..90c7b5ed9 100644 --- a/internal/photoprism/cleanup.go +++ b/internal/photoprism/cleanup.go @@ -64,7 +64,7 @@ func (w *CleanUp) Start(opt CleanUpOptions) (thumbs int, orphans int, err error) } // Thumbnails storage path. - thumbPath := w.conf.ThumbPath() + thumbPath := w.conf.ThumbCachePath() // Find and remove orphan thumbnail files. if err := fastwalk.Walk(thumbPath, func(fileName string, info os.FileMode) error { diff --git a/internal/photoprism/colors_test.go b/internal/photoprism/colors_test.go index eadbedcac..dd1b54c1d 100644 --- a/internal/photoprism/colors_test.go +++ b/internal/photoprism/colors_test.go @@ -104,7 +104,7 @@ func TestMediaFile_Colors(t *testing.T) { t.Run("cat_brown.jpg", func(t *testing.T) { if mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/cat_brown.jpg"); err == nil { - p, err := mediaFile.Colors(conf.ThumbPath()) + p, err := mediaFile.Colors(conf.ThumbCachePath()) t.Log(p, err) @@ -122,7 +122,7 @@ func TestMediaFile_Colors(t *testing.T) { t.Run("fern_green.jpg", func(t *testing.T) { if mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/fern_green.jpg"); err == nil { - p, err := mediaFile.Colors(conf.ThumbPath()) + p, err := mediaFile.Colors(conf.ThumbCachePath()) t.Log(p, err) @@ -140,7 +140,7 @@ func TestMediaFile_Colors(t *testing.T) { t.Run("IMG_4120.JPG", func(t *testing.T) { if mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120.JPG"); err == nil { - p, err := mediaFile.Colors(conf.ThumbPath()) + p, err := mediaFile.Colors(conf.ThumbCachePath()) t.Log(p, err) @@ -157,7 +157,7 @@ func TestMediaFile_Colors(t *testing.T) { t.Run("leaves_gold.jpg", func(t *testing.T) { if mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/leaves_gold.jpg"); err == nil { - p, err := mediaFile.Colors(conf.ThumbPath()) + p, err := mediaFile.Colors(conf.ThumbCachePath()) t.Log(p, err) @@ -175,7 +175,7 @@ func TestMediaFile_Colors(t *testing.T) { t.Run("Random.docx", func(t *testing.T) { mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/Random.docx") - p, err := mediaFile.Colors(conf.ThumbPath()) + p, err := mediaFile.Colors(conf.ThumbCachePath()) assert.Error(t, err, "no color information: not a JPEG file") t.Log(p) diff --git a/internal/photoprism/convert_avc.go b/internal/photoprism/convert_avc.go index e19c4852b..e24718560 100644 --- a/internal/photoprism/convert_avc.go +++ b/internal/photoprism/convert_avc.go @@ -77,6 +77,7 @@ func (c *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force var stderr bytes.Buffer cmd.Stdout = &out cmd.Stderr = &stderr + cmd.Env = []string{fmt.Sprintf("HOME=%s", c.conf.CmdCachePath())} event.Publish("index.converting", event.Data{ "fileType": f.FileType(), diff --git a/internal/photoprism/convert_avc_test.go b/internal/photoprism/convert_avc_test.go index a2f390626..44813ae11 100644 --- a/internal/photoprism/convert_avc_test.go +++ b/internal/photoprism/convert_avc_test.go @@ -65,3 +65,107 @@ func TestConvert_ToAvc(t *testing.T) { assert.Nil(t, avcFile) }) } + +func TestConvert_AvcBitrate(t *testing.T) { + conf := config.TestConfig() + convert := NewConvert(conf) + + t.Run("low", func(t *testing.T) { + fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") + + assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) + + mf, err := NewMediaFile(fileName) + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, "1M", convert.AvcBitrate(mf)) + }) + + t.Run("medium", func(t *testing.T) { + fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") + + assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) + + mf, err := NewMediaFile(fileName) + + if err != nil { + t.Fatal(err) + } + + mf.width = 1280 + mf.height = 1024 + + assert.Equal(t, "16M", convert.AvcBitrate(mf)) + }) + + t.Run("high", func(t *testing.T) { + fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") + + assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) + + mf, err := NewMediaFile(fileName) + + if err != nil { + t.Fatal(err) + } + + mf.width = 1920 + mf.height = 1080 + + assert.Equal(t, "25M", convert.AvcBitrate(mf)) + }) + + t.Run("very_high", func(t *testing.T) { + fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") + + assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) + + mf, err := NewMediaFile(fileName) + + if err != nil { + t.Fatal(err) + } + + mf.width = 4096 + mf.height = 2160 + + assert.Equal(t, "50M", convert.AvcBitrate(mf)) + }) +} + +func TestConvert_AvcConvertCommand(t *testing.T) { + conf := config.TestConfig() + convert := NewConvert(conf) + + t.Run(".mp4", func(t *testing.T) { + fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") + mf, err := NewMediaFile(fileName) + + if err != nil { + t.Fatal(err) + } + + r, _, err := convert.AvcConvertCommand(mf, "avc1", "") + + if err != nil { + t.Fatal(err) + } + assert.Contains(t, r.Path, "ffmpeg") + assert.Contains(t, r.Args, "mp4") + }) + t.Run(".jpg", func(t *testing.T) { + fileName := filepath.Join(conf.ExamplesPath(), "cat_black.jpg") + mf, err := NewMediaFile(fileName) + + if err != nil { + t.Fatal(err) + } + + r, _, err := convert.AvcConvertCommand(mf, "avc1", "") + assert.Error(t, err) + assert.Nil(t, r) + }) +} diff --git a/internal/photoprism/convert_jpeg.go b/internal/photoprism/convert_jpeg.go index de966a5ce..fc1540781 100644 --- a/internal/photoprism/convert_jpeg.go +++ b/internal/photoprism/convert_jpeg.go @@ -101,6 +101,7 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) { var stderr bytes.Buffer cmd.Stdout = &out cmd.Stderr = &stderr + cmd.Env = []string{fmt.Sprintf("HOME=%s", c.conf.CmdCachePath())} log.Infof("convert: converting %s to %s (%s)", clean.Log(filepath.Base(fileName)), clean.Log(filepath.Base(jpegName)), filepath.Base(cmd.Path)) @@ -135,7 +136,6 @@ func (c *Convert) JpegConvertCommand(f *MediaFile, jpegName string, xmpName stri result = exec.Command(c.conf.SipsBin(), "-Z", maxSize, "-s", "format", "jpeg", "--out", jpegName, f.FileName()) } else if f.IsRaw() && c.conf.RawEnabled() { if c.conf.DarktableEnabled() && c.darktableBlacklist.Ok(fileExt) { - cachePath, configPath := conf.DarktableCachePath(), conf.DarktableConfigPath() var args []string @@ -155,8 +155,16 @@ func (c *Convert) JpegConvertCommand(f *MediaFile, jpegName string, xmpName stri args = append(args, "--apply-custom-presets", "false", "--width", maxSize, "--height", maxSize, "--hq", "true", "--upscale", "false") } - // Set Darktable core storage paths. - args = append(args, "--core", "--configdir", configPath, "--cachedir", cachePath, "--library", ":memory:") + // Set library, config, and cache location. + args = append(args, "--core", "--library", ":memory:") + + if dir := conf.DarktableConfigPath(); dir != "" { + args = append(args, "--configdir", dir) + } + + if dir := conf.DarktableCachePath(); dir != "" { + args = append(args, "--cachedir", dir) + } result = exec.Command(c.conf.DarktableBin(), args...) } else if c.conf.RawtherapeeEnabled() && c.rawtherapeeBlacklist.Ok(fileExt) { diff --git a/internal/photoprism/convert_jpeg_test.go b/internal/photoprism/convert_jpeg_test.go new file mode 100644 index 000000000..3b5584618 --- /dev/null +++ b/internal/photoprism/convert_jpeg_test.go @@ -0,0 +1,106 @@ +package photoprism + +import ( + "os" + "path/filepath" + "testing" + + "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/pkg/fs" + "github.com/stretchr/testify/assert" +) + +func TestConvert_ToJpeg(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + conf := config.TestConfig() + conf.InitializeTestData(t) + convert := NewConvert(conf) + + t.Run("Video", func(t *testing.T) { + fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") + outputName := filepath.Join(conf.SidecarPath(), conf.ExamplesPath(), "gopher-video.mp4.jpg") + + _ = os.Remove(outputName) + + assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) + + mf, err := NewMediaFile(fileName) + + if err != nil { + t.Fatal(err) + } + + jpegFile, err := convert.ToJpeg(mf, false) + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, jpegFile.FileName(), outputName) + assert.Truef(t, fs.FileExists(jpegFile.FileName()), "output file does not exist: %s", jpegFile.FileName()) + + t.Logf("video metadata: %+v", jpegFile.MetaData()) + + _ = os.Remove(outputName) + }) + + t.Run("Raw", func(t *testing.T) { + jpegFilename := filepath.Join(conf.ImportPath(), "fern_green.jpg") + + assert.Truef(t, fs.FileExists(jpegFilename), "file does not exist: %s", jpegFilename) + + t.Logf("Testing RAW to JPEG convert with %s", jpegFilename) + + mf, err := NewMediaFile(jpegFilename) + + if err != nil { + t.Fatal(err) + } + + imageJpeg, err := convert.ToJpeg(mf, false) + + if err != nil { + t.Fatal(err) + } + + infoJpeg := imageJpeg.MetaData() + + assert.Equal(t, jpegFilename, imageJpeg.fileName) + + assert.Equal(t, "Canon EOS 7D", infoJpeg.CameraModel) + + rawFilename := filepath.Join(conf.ImportPath(), "raw", "IMG_2567.CR2") + jpgFilename := filepath.Join(conf.SidecarPath(), conf.ImportPath(), "raw/IMG_2567.CR2.jpg") + + t.Logf("Testing RAW to JPEG convert with %s", rawFilename) + + rawMediaFile, err := NewMediaFile(rawFilename) + + if err != nil { + t.Fatalf("%s for %s", err.Error(), rawFilename) + } + + imageRaw, err := convert.ToJpeg(rawMediaFile, false) + + if err != nil { + t.Fatalf("%s for %s", err.Error(), rawFilename) + } + + assert.True(t, fs.FileExists(jpgFilename), "Jpeg file was not found - is Darktable installed?") + + if imageRaw == nil { + t.Fatal("imageRaw is nil") + } + + assert.NotEqual(t, rawFilename, imageRaw.fileName) + + infoRaw := imageRaw.MetaData() + + assert.Equal(t, "Canon EOS 6D", infoRaw.CameraModel) + + _ = os.Remove(jpgFilename) + }) +} diff --git a/internal/photoprism/convert_json.go b/internal/photoprism/convert_json.go index 3abfde330..ceb945a72 100644 --- a/internal/photoprism/convert_json.go +++ b/internal/photoprism/convert_json.go @@ -37,6 +37,7 @@ func (c *Convert) ToJson(f *MediaFile) (jsonName string, err error) { var stderr bytes.Buffer cmd.Stdout = &out cmd.Stderr = &stderr + cmd.Env = []string{fmt.Sprintf("HOME=%s", c.conf.CmdCachePath())} // Log exact command for debugging in trace mode. log.Trace(cmd.String()) diff --git a/internal/photoprism/convert_json_test.go b/internal/photoprism/convert_json_test.go new file mode 100644 index 000000000..b37069ba8 --- /dev/null +++ b/internal/photoprism/convert_json_test.go @@ -0,0 +1,93 @@ +package photoprism + +import ( + "os" + "path/filepath" + "testing" + + "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/pkg/fs" + "github.com/stretchr/testify/assert" +) + +func TestConvert_ToJson(t *testing.T) { + conf := config.TestConfig() + convert := NewConvert(conf) + + t.Run("gopher-video.mp4", func(t *testing.T) { + fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") + + assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) + + mf, err := NewMediaFile(fileName) + + if err != nil { + t.Fatal(err) + } + + jsonName, err := convert.ToJson(mf) + + if err != nil { + t.Fatal(err) + } + + if jsonName == "" { + t.Fatal("json file name should not be empty") + } + + assert.FileExists(t, jsonName) + + _ = os.Remove(jsonName) + }) + + t.Run("IMG_4120.JPG", func(t *testing.T) { + fileName := filepath.Join(conf.ExamplesPath(), "IMG_4120.JPG") + assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) + + mf, err := NewMediaFile(fileName) + + if err != nil { + t.Fatal(err) + } + + jsonName, err := convert.ToJson(mf) + + if err != nil { + t.Fatal(err) + } + + if jsonName == "" { + t.Fatal("json file name should not be empty") + } + + assert.FileExists(t, jsonName) + + _ = os.Remove(jsonName) + }) + + t.Run("iphone_7.heic", func(t *testing.T) { + fileName := conf.ExamplesPath() + "/iphone_7.heic" + + assert.True(t, fs.FileExists(fileName)) + + mf, err := NewMediaFile(fileName) + + if err != nil { + t.Fatal(err) + } + + jsonName, err := convert.ToJson(mf) + + if err != nil { + t.Fatal(err) + } + + if jsonName == "" { + t.Fatal("json file name should not be empty") + } + + assert.FileExists(t, jsonName) + + _ = os.Remove(jsonName) + }) +} diff --git a/internal/photoprism/convert_test.go b/internal/photoprism/convert_test.go index 0b515e532..a51532bdf 100644 --- a/internal/photoprism/convert_test.go +++ b/internal/photoprism/convert_test.go @@ -18,183 +18,6 @@ func TestNewConvert(t *testing.T) { assert.IsType(t, &Convert{}, convert) } -func TestConvert_ToJpeg(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - } - - conf := config.TestConfig() - conf.InitializeTestData(t) - convert := NewConvert(conf) - - t.Run("gopher-video.mp4", func(t *testing.T) { - fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") - outputName := filepath.Join(conf.SidecarPath(), conf.ExamplesPath(), "gopher-video.mp4.jpg") - - _ = os.Remove(outputName) - - assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) - - mf, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - jpegFile, err := convert.ToJpeg(mf, false) - - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, jpegFile.FileName(), outputName) - assert.Truef(t, fs.FileExists(jpegFile.FileName()), "output file does not exist: %s", jpegFile.FileName()) - - t.Logf("video metadata: %+v", jpegFile.MetaData()) - - _ = os.Remove(outputName) - }) - - t.Run("fern_green.jpg", func(t *testing.T) { - jpegFilename := filepath.Join(conf.ImportPath(), "fern_green.jpg") - - assert.Truef(t, fs.FileExists(jpegFilename), "file does not exist: %s", jpegFilename) - - t.Logf("Testing RAW to JPEG convert with %s", jpegFilename) - - mf, err := NewMediaFile(jpegFilename) - - if err != nil { - t.Fatal(err) - } - - imageJpeg, err := convert.ToJpeg(mf, false) - - if err != nil { - t.Fatal(err) - } - - infoJpeg := imageJpeg.MetaData() - - assert.Equal(t, jpegFilename, imageJpeg.fileName) - - assert.Equal(t, "Canon EOS 7D", infoJpeg.CameraModel) - - rawFilename := filepath.Join(conf.ImportPath(), "raw", "IMG_2567.CR2") - jpgFilename := filepath.Join(conf.SidecarPath(), conf.ImportPath(), "raw/IMG_2567.CR2.jpg") - - t.Logf("Testing RAW to JPEG convert with %s", rawFilename) - - rawMediaFile, err := NewMediaFile(rawFilename) - - if err != nil { - t.Fatalf("%s for %s", err.Error(), rawFilename) - } - - imageRaw, err := convert.ToJpeg(rawMediaFile, false) - - if err != nil { - t.Fatalf("%s for %s", err.Error(), rawFilename) - } - - assert.True(t, fs.FileExists(jpgFilename), "Jpeg file was not found - is Darktable installed?") - - if imageRaw == nil { - t.Fatal("imageRaw is nil") - } - - assert.NotEqual(t, rawFilename, imageRaw.fileName) - - infoRaw := imageRaw.MetaData() - - assert.Equal(t, "Canon EOS 6D", infoRaw.CameraModel) - - _ = os.Remove(jpgFilename) - }) -} - -func TestConvert_ToJson(t *testing.T) { - conf := config.TestConfig() - convert := NewConvert(conf) - - t.Run("gopher-video.mp4", func(t *testing.T) { - fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") - - assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) - - mf, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - jsonName, err := convert.ToJson(mf) - - if err != nil { - t.Fatal(err) - } - - if jsonName == "" { - t.Fatal("json file name should not be empty") - } - - assert.FileExists(t, jsonName) - - _ = os.Remove(jsonName) - }) - - t.Run("IMG_4120.JPG", func(t *testing.T) { - fileName := filepath.Join(conf.ExamplesPath(), "IMG_4120.JPG") - assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) - - mf, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - jsonName, err := convert.ToJson(mf) - - if err != nil { - t.Fatal(err) - } - - if jsonName == "" { - t.Fatal("json file name should not be empty") - } - - assert.FileExists(t, jsonName) - - _ = os.Remove(jsonName) - }) - - t.Run("iphone_7.heic", func(t *testing.T) { - fileName := conf.ExamplesPath() + "/iphone_7.heic" - - assert.True(t, fs.FileExists(fileName)) - - mf, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - jsonName, err := convert.ToJson(mf) - - if err != nil { - t.Fatal(err) - } - - if jsonName == "" { - t.Fatal("json file name should not be empty") - } - - assert.FileExists(t, jsonName) - - _ = os.Remove(jsonName) - }) -} - func TestConvert_Start(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") @@ -244,107 +67,3 @@ func TestConvert_Start(t *testing.T) { assert.NotEqual(t, oldHash, newHash, "Fingerprint of old and new JPEG file must not be the same") } - -func TestConvert_AvcBitrate(t *testing.T) { - conf := config.TestConfig() - convert := NewConvert(conf) - - t.Run("low", func(t *testing.T) { - fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") - - assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) - - mf, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, "1M", convert.AvcBitrate(mf)) - }) - - t.Run("medium", func(t *testing.T) { - fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") - - assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) - - mf, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - mf.width = 1280 - mf.height = 1024 - - assert.Equal(t, "16M", convert.AvcBitrate(mf)) - }) - - t.Run("high", func(t *testing.T) { - fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") - - assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) - - mf, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - mf.width = 1920 - mf.height = 1080 - - assert.Equal(t, "25M", convert.AvcBitrate(mf)) - }) - - t.Run("very_high", func(t *testing.T) { - fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") - - assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName) - - mf, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - mf.width = 4096 - mf.height = 2160 - - assert.Equal(t, "50M", convert.AvcBitrate(mf)) - }) -} - -func TestConvert_AvcConvertCommand(t *testing.T) { - conf := config.TestConfig() - convert := NewConvert(conf) - - t.Run(".mp4", func(t *testing.T) { - fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4") - mf, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - r, _, err := convert.AvcConvertCommand(mf, "avc1", "") - - if err != nil { - t.Fatal(err) - } - assert.Contains(t, r.Path, "ffmpeg") - assert.Contains(t, r.Args, "mp4") - }) - t.Run(".jpg", func(t *testing.T) { - fileName := filepath.Join(conf.ExamplesPath(), "cat_black.jpg") - mf, err := NewMediaFile(fileName) - - if err != nil { - t.Fatal(err) - } - - r, _, err := convert.AvcConvertCommand(mf, "avc1", "") - assert.Error(t, err) - assert.Nil(t, r) - }) -} diff --git a/internal/photoprism/import.go b/internal/photoprism/import.go index 48eb377c0..697d135c5 100644 --- a/internal/photoprism/import.go +++ b/internal/photoprism/import.go @@ -47,7 +47,7 @@ func (imp *Import) originalsPath() string { // thumbPath returns the thumbnails cache path as string. func (imp *Import) thumbPath() string { - return imp.conf.ThumbPath() + return imp.conf.ThumbCachePath() } // Start imports media files from a directory and converts/indexes them as needed. diff --git a/internal/photoprism/index.go b/internal/photoprism/index.go index 735c558f3..8cd1b00f9 100644 --- a/internal/photoprism/index.go +++ b/internal/photoprism/index.go @@ -63,7 +63,7 @@ func (ind *Index) originalsPath() string { } func (ind *Index) thumbPath() string { - return ind.conf.ThumbPath() + return ind.conf.ThumbCachePath() } // Cancel stops the current indexing operation. diff --git a/internal/photoprism/index_faces.go b/internal/photoprism/index_faces.go index 060d1d1a7..d7635d75f 100644 --- a/internal/photoprism/index_faces.go +++ b/internal/photoprism/index_faces.go @@ -25,7 +25,7 @@ func (ind *Index) Faces(jpeg *MediaFile, expected int) face.Faces { thumbSize = thumb.Fit1280 } - thumbName, err := jpeg.Thumbnail(Config().ThumbPath(), thumbSize) + thumbName, err := jpeg.Thumbnail(Config().ThumbCachePath(), thumbSize) if err != nil { log.Debugf("index: %s in %s (faces)", err, clean.Log(jpeg.BaseName())) diff --git a/internal/photoprism/index_labels.go b/internal/photoprism/index_labels.go index 214e47845..8e0b662b3 100644 --- a/internal/photoprism/index_labels.go +++ b/internal/photoprism/index_labels.go @@ -24,7 +24,7 @@ func (ind *Index) Labels(jpeg *MediaFile) (results classify.Labels) { var labels classify.Labels for _, size := range sizes { - filename, err := jpeg.Thumbnail(Config().ThumbPath(), size) + filename, err := jpeg.Thumbnail(Config().ThumbCachePath(), size) if err != nil { log.Debugf("%s in %s", err, clean.Log(jpeg.BaseName())) diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index 0736ad2c3..cee22389a 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -354,7 +354,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID switch { case m.IsJpeg(): // Color information - if p, err := m.Colors(Config().ThumbPath()); err != nil { + if p, err := m.Colors(Config().ThumbCachePath()); err != nil { log.Debugf("%s while detecting colors", err.Error()) file.FileError = err.Error() file.FilePrimary = false diff --git a/internal/photoprism/index_nsfw.go b/internal/photoprism/index_nsfw.go index 099f2b68c..25d22992c 100644 --- a/internal/photoprism/index_nsfw.go +++ b/internal/photoprism/index_nsfw.go @@ -8,7 +8,7 @@ import ( // NSFW returns true if media file might be offensive and detection is enabled. func (ind *Index) NSFW(m *MediaFile) bool { - filename, err := m.Thumbnail(Config().ThumbPath(), thumb.Fit720) + filename, err := m.Thumbnail(Config().ThumbCachePath(), thumb.Fit720) if err != nil { log.Error(err) diff --git a/internal/photoprism/resample.go b/internal/photoprism/resample.go index a2c241298..bcdd8f22d 100644 --- a/internal/photoprism/resample.go +++ b/internal/photoprism/resample.go @@ -40,7 +40,7 @@ func (w *Resample) Start(force bool) (err error) { defer mutex.MainWorker.Stop() originalsPath := w.conf.OriginalsPath() - thumbnailsPath := w.conf.ThumbPath() + thumbnailsPath := w.conf.ThumbCachePath() jobs := make(chan ResampleJob) diff --git a/internal/workers/share.go b/internal/workers/share.go index c5acd8dd8..7b626404d 100644 --- a/internal/workers/share.go +++ b/internal/workers/share.go @@ -105,7 +105,7 @@ func (worker *Share) Start() (err error) { continue } - srcFileName, err = thumb.FromFile(srcFileName, file.File.FileHash, worker.conf.ThumbPath(), size.Width, size.Height, file.File.FileOrientation, size.Options...) + srcFileName, err = thumb.FromFile(srcFileName, file.File.FileHash, worker.conf.ThumbCachePath(), size.Width, size.Height, file.File.FileOrientation, size.Options...) if err != nil { worker.logError(err)