CLI: Create thumbs and convert files in deterministic order #3194
This also adds support for specifying a path to the thumbs command. Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
4e0f38881d
commit
0c4aa86f85
|
@ -57,7 +57,7 @@ func StartImport(router *gin.RouterGroup) {
|
||||||
srcFolder := ""
|
srcFolder := ""
|
||||||
importPath := conf.ImportPath()
|
importPath := conf.ImportPath()
|
||||||
|
|
||||||
// Import from sub-folder?
|
// Import from subfolder?
|
||||||
if srcFolder = c.Param("path"); srcFolder != "" && srcFolder != "/" {
|
if srcFolder = c.Param("path"); srcFolder != "" && srcFolder != "/" {
|
||||||
srcFolder = clean.UserPath(srcFolder)
|
srcFolder = clean.UserPath(srcFolder)
|
||||||
} else if f.Path != "" {
|
} else if f.Path != "" {
|
||||||
|
|
|
@ -24,7 +24,7 @@ const backupDescription = "A user-defined SQL dump FILENAME or - for stdout can
|
||||||
" Make sure to run the command with exec -T when using Docker to prevent log messages from being sent to stdout.\n" +
|
" Make sure to run the command with exec -T when using Docker to prevent log messages from being sent to stdout.\n" +
|
||||||
" The index backup and album file paths are automatically detected if not specified explicitly."
|
" The index backup and album file paths are automatically detected if not specified explicitly."
|
||||||
|
|
||||||
// BackupCommand configures the backup cli command.
|
// BackupCommand configures the command name, flags, and action.
|
||||||
var BackupCommand = cli.Command{
|
var BackupCommand = cli.Command{
|
||||||
Name: "backup",
|
Name: "backup",
|
||||||
Description: backupDescription,
|
Description: backupDescription,
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CleanUpCommand registers the cleanup command.
|
// CleanUpCommand configures the command name, flags, and action.
|
||||||
var CleanUpCommand = cli.Command{
|
var CleanUpCommand = cli.Command{
|
||||||
Name: "cleanup",
|
Name: "cleanup",
|
||||||
Usage: "Removes orphaned index entries, sidecar and thumbnail files",
|
Usage: "Removes orphaned index entries, sidecar and thumbnail files",
|
||||||
|
|
|
@ -13,11 +13,11 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConvertCommand registers the convert cli command.
|
// ConvertCommand configures the command name, flags, and action.
|
||||||
var ConvertCommand = cli.Command{
|
var ConvertCommand = cli.Command{
|
||||||
Name: "convert",
|
Name: "convert",
|
||||||
Usage: "Converts files in other formats to JPEG and AVC as needed",
|
Usage: "Converts files in other formats to JPEG and AVC as needed",
|
||||||
ArgsUsage: "[sub-folder]",
|
ArgsUsage: "[subfolder]",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringSliceFlag{
|
cli.StringSliceFlag{
|
||||||
Name: "ext, e",
|
Name: "ext, e",
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CopyCommand registers the copy cli command.
|
// CopyCommand configures the command name, flags, and action.
|
||||||
var CopyCommand = cli.Command{
|
var CopyCommand = cli.Command{
|
||||||
Name: "cp",
|
Name: "cp",
|
||||||
Aliases: []string{"copy"},
|
Aliases: []string{"copy"},
|
||||||
|
|
|
@ -18,7 +18,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FacesCommand registers the face recognition subcommands.
|
// FacesCommand configures the command name, flags, and action.
|
||||||
var FacesCommand = cli.Command{
|
var FacesCommand = cli.Command{
|
||||||
Name: "faces",
|
Name: "faces",
|
||||||
Usage: "Face recognition subcommands",
|
Usage: "Face recognition subcommands",
|
||||||
|
@ -53,7 +53,7 @@ var FacesCommand = cli.Command{
|
||||||
{
|
{
|
||||||
Name: "index",
|
Name: "index",
|
||||||
Usage: "Searches originals for faces",
|
Usage: "Searches originals for faces",
|
||||||
ArgsUsage: "[sub-folder]",
|
ArgsUsage: "[subfolder]",
|
||||||
Action: facesIndexAction,
|
Action: facesIndexAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImportCommand registers the import cli command.
|
// ImportCommand configures the command name, flags, and action.
|
||||||
var ImportCommand = cli.Command{
|
var ImportCommand = cli.Command{
|
||||||
Name: "mv",
|
Name: "mv",
|
||||||
Aliases: []string{"import"},
|
Aliases: []string{"import"},
|
||||||
|
|
|
@ -19,7 +19,7 @@ import (
|
||||||
var IndexCommand = cli.Command{
|
var IndexCommand = cli.Command{
|
||||||
Name: "index",
|
Name: "index",
|
||||||
Usage: "Indexes original media files",
|
Usage: "Indexes original media files",
|
||||||
ArgsUsage: "[sub-folder]",
|
ArgsUsage: "[subfolder]",
|
||||||
Flags: indexFlags,
|
Flags: indexFlags,
|
||||||
Action: indexAction,
|
Action: indexAction,
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/get"
|
"github.com/photoprism/photoprism/internal/get"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MomentsCommand registers the moments command.
|
// MomentsCommand configures the command name, flags, and action.
|
||||||
var MomentsCommand = cli.Command{
|
var MomentsCommand = cli.Command{
|
||||||
Name: "moments",
|
Name: "moments",
|
||||||
Usage: "Creates albums of special moments, trips, and places",
|
Usage: "Creates albums of special moments, trips, and places",
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/workers"
|
"github.com/photoprism/photoprism/internal/workers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OptimizeCommand registers the index cli command.
|
// OptimizeCommand configures the command name, flags, and action.
|
||||||
var OptimizeCommand = cli.Command{
|
var OptimizeCommand = cli.Command{
|
||||||
Name: "optimize",
|
Name: "optimize",
|
||||||
Usage: "Maintains titles, estimates, and other metadata",
|
Usage: "Maintains titles, estimates, and other metadata",
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PlacesCommand registers the places subcommands.
|
// PlacesCommand configures the command name, flags, and action.
|
||||||
var PlacesCommand = cli.Command{
|
var PlacesCommand = cli.Command{
|
||||||
Name: "places",
|
Name: "places",
|
||||||
Usage: "Maps and location details subcommands",
|
Usage: "Maps and location details subcommands",
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PurgeCommand registers the index cli command.
|
// PurgeCommand configures the command name, flags, and action.
|
||||||
var PurgeCommand = cli.Command{
|
var PurgeCommand = cli.Command{
|
||||||
Name: "purge",
|
Name: "purge",
|
||||||
Usage: "Updates missing files, photo counts, and album covers",
|
Usage: "Updates missing files, photo counts, and album covers",
|
||||||
|
|
|
@ -18,7 +18,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResetCommand resets the index, clears the cache, and removes sidecar files after confirmation.
|
// ResetCommand configures the command name, flags, and action.
|
||||||
var ResetCommand = cli.Command{
|
var ResetCommand = cli.Command{
|
||||||
Name: "reset",
|
Name: "reset",
|
||||||
Usage: "Resets the index, clears the cache, and removes sidecar files",
|
Usage: "Resets the index, clears the cache, and removes sidecar files",
|
||||||
|
|
|
@ -26,7 +26,7 @@ const restoreDescription = "A user-defined SQL dump FILENAME can be passed as th
|
||||||
"The -i parameter can be omitted in this case.\n" +
|
"The -i parameter can be omitted in this case.\n" +
|
||||||
" The index backup and album file paths are automatically detected if not specified explicitly."
|
" The index backup and album file paths are automatically detected if not specified explicitly."
|
||||||
|
|
||||||
// RestoreCommand configures the backup cli command.
|
// RestoreCommand configures the command name, flags, and action.
|
||||||
var RestoreCommand = cli.Command{
|
var RestoreCommand = cli.Command{
|
||||||
Name: "restore",
|
Name: "restore",
|
||||||
Description: restoreDescription,
|
Description: restoreDescription,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ShowCommand registers the show subcommands.
|
// ShowCommand configures the show subcommands.
|
||||||
var ShowCommand = cli.Command{
|
var ShowCommand = cli.Command{
|
||||||
Name: "show",
|
Name: "show",
|
||||||
Usage: "Shows supported formats, features, and config options",
|
Usage: "Shows supported formats, features, and config options",
|
||||||
|
|
|
@ -24,7 +24,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartCommand registers the start cli command.
|
// StartCommand configures the command name, flags, and action.
|
||||||
var StartCommand = cli.Command{
|
var StartCommand = cli.Command{
|
||||||
Name: "start",
|
Name: "start",
|
||||||
Aliases: []string{"up"},
|
Aliases: []string{"up"},
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusCommand registers the status command.
|
// StatusCommand configures the command name, flags, and action.
|
||||||
var StatusCommand = cli.Command{
|
var StatusCommand = cli.Command{
|
||||||
Name: "status",
|
Name: "status",
|
||||||
Usage: "Checks if the Web server is running",
|
Usage: "Checks if the Web server is running",
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StopCommand registers the stop cli command.
|
// StopCommand configures the command name, flags, and action.
|
||||||
var StopCommand = cli.Command{
|
var StopCommand = cli.Command{
|
||||||
Name: "stop",
|
Name: "stop",
|
||||||
Aliases: []string{"down"},
|
Aliases: []string{"down"},
|
||||||
|
|
|
@ -2,6 +2,7 @@ package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
@ -10,24 +11,25 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ThumbsCommand registers the resample cli command.
|
// ThumbsCommand configures the command name, flags, and action.
|
||||||
var ThumbsCommand = cli.Command{
|
var ThumbsCommand = cli.Command{
|
||||||
Name: "thumbs",
|
Name: "thumbs",
|
||||||
Usage: "Generates thumbnails using the current settings",
|
Usage: "Generates thumbnails using the current settings",
|
||||||
|
ArgsUsage: "[subfolder]",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "force, f",
|
Name: "force, f",
|
||||||
Usage: "replace existing thumbnails",
|
Usage: "replace existing thumbnail files",
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "originals, o",
|
Name: "originals, o",
|
||||||
Usage: "originals only, skip sidecar files",
|
Usage: "scan originals only, skip sidecar folder",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: thumbsAction,
|
Action: thumbsAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
// thumbsAction pre-renders thumbnail images.
|
// thumbsAction generates thumbnails using the current settings.
|
||||||
func thumbsAction(ctx *cli.Context) error {
|
func thumbsAction(ctx *cli.Context) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
@ -43,16 +45,41 @@ func thumbsAction(ctx *cli.Context) error {
|
||||||
conf.RegisterDb()
|
conf.RegisterDb()
|
||||||
defer conf.Shutdown()
|
defer conf.Shutdown()
|
||||||
|
|
||||||
log.Infof("creating thumbs in %s", clean.Log(conf.ThumbCachePath()))
|
dir := strings.TrimSpace(ctx.Args().First())
|
||||||
|
force := ctx.Bool("force")
|
||||||
|
originals := ctx.Bool("originals")
|
||||||
|
|
||||||
rs := get.Thumbs()
|
var action, ack string
|
||||||
|
if force {
|
||||||
|
action = "replacing"
|
||||||
|
ack = "replaced"
|
||||||
|
} else {
|
||||||
|
action = "creating"
|
||||||
|
ack = "created"
|
||||||
|
}
|
||||||
|
|
||||||
if err := rs.Start(ctx.Bool("force"), ctx.Bool("originals")); err != nil {
|
// Display info.
|
||||||
log.Error(err)
|
if dir == "" {
|
||||||
|
if originals {
|
||||||
|
log.Infof("%s thumbnails for originals only", action)
|
||||||
|
} else {
|
||||||
|
log.Infof("%s thumbnails for originals and sidecar files", action)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if originals {
|
||||||
|
log.Infof("%s thumbnails for originals in %s", action, clean.LogQuote(dir))
|
||||||
|
} else {
|
||||||
|
log.Infof("%s thumbnails for originals and sidecar files in %s", action, clean.LogQuote(dir))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w := get.Thumbs()
|
||||||
|
|
||||||
|
if err = w.Start(dir, ctx.Bool("force"), ctx.Bool("originals")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("thumbs created in %s", time.Since(start))
|
log.Infof("thumbnails %s in %s", ack, time.Since(start))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ const (
|
||||||
UserWebDAVUsage = "allow to sync files via WebDAV"
|
UserWebDAVUsage = "allow to sync files via WebDAV"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UsersCommand registers the user management subcommands.
|
// UsersCommand configures the user management subcommands.
|
||||||
var UsersCommand = cli.Command{
|
var UsersCommand = cli.Command{
|
||||||
Name: "users",
|
Name: "users",
|
||||||
Aliases: []string{"user"},
|
Aliases: []string{"user"},
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VersionCommand registers the version cli command.
|
// VersionCommand configures the command name, flags, and action.
|
||||||
var VersionCommand = cli.Command{
|
var VersionCommand = cli.Command{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Usage: "Shows version information",
|
Usage: "Shows version information",
|
||||||
|
|
|
@ -40,7 +40,7 @@ func NewConvert(conf *config.Config) *Convert {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start converts all files in a directory to JPEG if possible.
|
// Start converts all files in a directory to JPEG if possible.
|
||||||
func (c *Convert) Start(path string, ext []string, force bool) (err error) {
|
func (c *Convert) Start(dir string, ext []string, force bool) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = fmt.Errorf("convert: %s (panic)\nstack: %s", r, debug.Stack())
|
err = fmt.Errorf("convert: %s (panic)\nstack: %s", r, debug.Stack())
|
||||||
|
@ -48,7 +48,7 @@ func (c *Convert) Start(path string, ext []string, force bool) (err error) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := mutex.MainWorker.Start(); err != nil {
|
if err = mutex.MainWorker.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ func (c *Convert) Start(path string, ext []string, force bool) (err error) {
|
||||||
done := make(fs.Done)
|
done := make(fs.Done)
|
||||||
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)
|
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)
|
||||||
|
|
||||||
if err := ignore.Dir(path); err != nil {
|
if err = ignore.Dir(dir); err != nil {
|
||||||
log.Infof("convert: %s", err)
|
log.Infof("convert: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ func (c *Convert) Start(path string, ext []string, force bool) (err error) {
|
||||||
log.Infof("convert: ignoring %s", clean.Log(filepath.Base(fileName)))
|
log.Infof("convert: ignoring %s", clean.Log(filepath.Base(fileName)))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = godirwalk.Walk(path, &godirwalk.Options{
|
err = godirwalk.Walk(dir, &godirwalk.Options{
|
||||||
ErrorCallback: func(fileName string, err error) godirwalk.ErrorAction {
|
ErrorCallback: func(fileName string, err error) godirwalk.ErrorAction {
|
||||||
return godirwalk.SkipNode
|
return godirwalk.SkipNode
|
||||||
},
|
},
|
||||||
|
@ -122,7 +122,7 @@ func (c *Convert) Start(path string, ext []string, force bool) (err error) {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Unsorted: true,
|
Unsorted: false,
|
||||||
FollowSymbolicLinks: true,
|
FollowSymbolicLinks: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
|
||||||
|
|
||||||
// Check if the import folder exists.
|
// Check if the import folder exists.
|
||||||
if !fs.PathExists(importPath) {
|
if !fs.PathExists(importPath) {
|
||||||
event.Error(fmt.Sprintf("import: %s does not exist", importPath))
|
event.Error(fmt.Sprintf("import: directory %s not found", importPath))
|
||||||
return done
|
return done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ func (ind *Index) Start(o IndexOptions) fs.Done {
|
||||||
optionsPath := filepath.Join(originalsPath, o.Path)
|
optionsPath := filepath.Join(originalsPath, o.Path)
|
||||||
|
|
||||||
if !fs.PathExists(optionsPath) {
|
if !fs.PathExists(optionsPath) {
|
||||||
event.Error(fmt.Sprintf("%s does not exist", clean.Log(optionsPath)))
|
event.Error(fmt.Sprintf("index: directory %s not found", clean.Log(optionsPath)))
|
||||||
return done
|
return done
|
||||||
} else if fs.DirIsEmpty(originalsPath) {
|
} else if fs.DirIsEmpty(originalsPath) {
|
||||||
event.InfoMsg(i18n.ErrOriginalsEmpty)
|
event.InfoMsg(i18n.ErrOriginalsEmpty)
|
||||||
|
|
|
@ -721,53 +721,96 @@ func (m *MediaFile) IsPreviewImage() bool {
|
||||||
return m.IsJpeg() || m.IsPng()
|
return m.IsJpeg() || m.IsPng()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsJpeg return true if this media file is a JPEG image.
|
// IsJpeg checks if the file is a JPEG image with a supported file type extension.
|
||||||
func (m *MediaFile) IsJpeg() bool {
|
func (m *MediaFile) IsJpeg() bool {
|
||||||
// Don't import/use existing thumbnail files (we create our own)
|
|
||||||
if m.Extension() == fs.ExtTHM {
|
if m.Extension() == fs.ExtTHM {
|
||||||
|
// Ignore .thm files, as some cameras automatically
|
||||||
|
// create them as thumbnails.
|
||||||
|
return false
|
||||||
|
} else if fs.FileType(m.fileName) != fs.ImageJPEG {
|
||||||
|
// Files with an incorrect file extension are no longer
|
||||||
|
// recognized as JPEG to improve indexing performance.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Since mime type detection is expensive, it is only
|
||||||
|
// performed after other checks have passed.
|
||||||
return m.MimeType() == fs.MimeTypeJpeg
|
return m.MimeType() == fs.MimeTypeJpeg
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPng returns true if this is a PNG image.
|
// IsPng checks if the file is a PNG image with a supported file type extension.
|
||||||
func (m *MediaFile) IsPng() bool {
|
func (m *MediaFile) IsPng() bool {
|
||||||
|
if fs.FileType(m.fileName) != fs.ImagePNG {
|
||||||
|
// Files with an incorrect file extension are no longer
|
||||||
|
// recognized as PNG to improve indexing performance.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since mime type detection is expensive, it is only
|
||||||
|
// performed after other checks have passed.
|
||||||
return m.MimeType() == fs.MimeTypePng
|
return m.MimeType() == fs.MimeTypePng
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsGif returns true if this is a GIF image.
|
// IsGif checks if the file is a GIF image with a supported file type extension.
|
||||||
func (m *MediaFile) IsGif() bool {
|
func (m *MediaFile) IsGif() bool {
|
||||||
|
if fs.FileType(m.fileName) != fs.ImageGIF {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return m.MimeType() == fs.MimeTypeGif
|
return m.MimeType() == fs.MimeTypeGif
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTiff returns true if this is a TIFF image.
|
// IsTiff checks if the file is a TIFF image with a supported file type extension.
|
||||||
func (m *MediaFile) IsTiff() bool {
|
func (m *MediaFile) IsTiff() bool {
|
||||||
return m.HasFileType(fs.ImageTIFF) && m.MimeType() == fs.MimeTypeTiff
|
if fs.FileType(m.fileName) != fs.ImageTIFF {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.MimeType() == fs.MimeTypeTiff
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDNG returns true if this is a Adobe Digital Negative image.
|
// IsDNG checks if the file is a Adobe Digital Negative (DNG) image with a supported file type extension.
|
||||||
func (m *MediaFile) IsDNG() bool {
|
func (m *MediaFile) IsDNG() bool {
|
||||||
|
if fs.FileType(m.fileName) != fs.ImageDNG {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return m.MimeType() == fs.MimeTypeDNG
|
return m.MimeType() == fs.MimeTypeDNG
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsHEIC returns true if this is a High Efficiency Image File Format image.
|
// IsHEIC checks if the file is a High Efficiency Image File Format (HEIC/HEIF) image with a supported file type extension.
|
||||||
func (m *MediaFile) IsHEIC() bool {
|
func (m *MediaFile) IsHEIC() bool {
|
||||||
|
if t := fs.FileType(m.fileName); t != fs.ImageHEIC && t != fs.ImageHEIF {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return m.MimeType() == fs.MimeTypeHEIC
|
return m.MimeType() == fs.MimeTypeHEIC
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAVIF returns true if this is an AV1 Image File Format image.
|
// IsAVIF checks if the file is an AV1 Image File Format image with a supported file type extension.
|
||||||
func (m *MediaFile) IsAVIF() bool {
|
func (m *MediaFile) IsAVIF() bool {
|
||||||
|
if fs.FileType(m.fileName) != fs.ImageAVIF {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return m.MimeType() == fs.MimeTypeAVIF
|
return m.MimeType() == fs.MimeTypeAVIF
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsBitmap returns true if this is a bitmap image.
|
// IsBitmap checks if the file is a bitmap image with a supported file type extension.
|
||||||
func (m *MediaFile) IsBitmap() bool {
|
func (m *MediaFile) IsBitmap() bool {
|
||||||
|
if fs.FileType(m.fileName) != fs.ImageBMP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return m.MimeType() == fs.MimeTypeBitmap
|
return m.MimeType() == fs.MimeTypeBitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWebP returns true if this is a WebP image file.
|
// IsWebP checks if the file is a WebP image file with a supported file type extension.
|
||||||
func (m *MediaFile) IsWebP() bool {
|
func (m *MediaFile) IsWebP() bool {
|
||||||
|
if fs.FileType(m.fileName) != fs.ImageWebP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return m.MimeType() == fs.MimeTypeWebP
|
return m.MimeType() == fs.MimeTypeWebP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -780,12 +823,12 @@ func (m *MediaFile) Duration() time.Duration {
|
||||||
return m.MetaData().Duration
|
return m.MetaData().Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAnimatedGif returns true if it is an animated GIF.
|
// IsAnimatedGif checks if the file is an animated GIF with a supported file type extension.
|
||||||
func (m *MediaFile) IsAnimatedGif() bool {
|
func (m *MediaFile) IsAnimatedGif() bool {
|
||||||
return m.IsGif() && m.MetaData().Frames > 1
|
return m.IsGif() && m.MetaData().Frames > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsJson return true if this media file is a json sidecar file.
|
// IsJson checks if the file is a JSON sidecar file with a supported file type extension.
|
||||||
func (m *MediaFile) IsJson() bool {
|
func (m *MediaFile) IsJson() bool {
|
||||||
return m.HasFileType(fs.SidecarJSON)
|
return m.HasFileType(fs.SidecarJSON)
|
||||||
}
|
}
|
||||||
|
@ -848,7 +891,7 @@ func (m *MediaFile) IsAnimated() bool {
|
||||||
|
|
||||||
// IsVideo returns true if this is a video file.
|
// IsVideo returns true if this is a video file.
|
||||||
func (m *MediaFile) IsVideo() bool {
|
func (m *MediaFile) IsVideo() bool {
|
||||||
return strings.HasPrefix(m.MimeType(), "video/") || m.Media() == media.Video
|
return m.HasMediaType(media.Video)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsVector returns true if this is a vector graphics.
|
// IsVector returns true if this is a vector graphics.
|
||||||
|
@ -863,7 +906,7 @@ func (m *MediaFile) IsSidecar() bool {
|
||||||
|
|
||||||
// IsSVG returns true if this is a SVG vector graphics.
|
// IsSVG returns true if this is a SVG vector graphics.
|
||||||
func (m *MediaFile) IsSVG() bool {
|
func (m *MediaFile) IsSVG() bool {
|
||||||
return m.HasFileType(fs.VectorSVG)
|
return m.FileType() == fs.VectorSVG
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsXMP returns true if this is a XMP sidecar file.
|
// IsXMP returns true if this is a XMP sidecar file.
|
||||||
|
@ -964,25 +1007,17 @@ func (m *MediaFile) HasPreviewImage() bool {
|
||||||
|
|
||||||
jpegName := fs.ImageJPEG.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
jpegName := fs.ImageJPEG.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||||
|
|
||||||
if jpegName == "" {
|
if m.hasPreviewImage = fs.MimeType(jpegName) == fs.MimeTypeJpeg; m.hasPreviewImage {
|
||||||
m.hasPreviewImage = false
|
|
||||||
} else {
|
|
||||||
m.hasPreviewImage = fs.MimeType(jpegName) == fs.MimeTypeJpeg
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.hasPreviewImage {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
pngName := fs.ImagePNG.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
pngName := fs.ImagePNG.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||||
|
|
||||||
if pngName == "" {
|
if m.hasPreviewImage = fs.MimeType(pngName) == fs.MimeTypePng; m.hasPreviewImage {
|
||||||
m.hasPreviewImage = false
|
return true
|
||||||
} else {
|
|
||||||
m.hasPreviewImage = fs.MimeType(pngName) == fs.MimeTypePng
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.hasPreviewImage
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MediaFile) decodeDimensions() error {
|
func (m *MediaFile) decodeDimensions() error {
|
||||||
|
|
|
@ -2139,9 +2139,12 @@ func TestMediaFile_FileType(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.True(t, m.IsJpeg())
|
// No longer recognized as JPEG to improve indexing performance (skips mime type detection).
|
||||||
assert.Equal(t, "jpg", string(m.FileType()))
|
assert.False(t, m.IsJpeg())
|
||||||
assert.Equal(t, fs.ImageJPEG, m.FileType())
|
assert.False(t, m.IsPng())
|
||||||
|
assert.Equal(t, "png", string(m.FileType()))
|
||||||
|
assert.Equal(t, "image/jpeg", m.MimeType())
|
||||||
|
assert.Equal(t, fs.ImagePNG, m.FileType())
|
||||||
assert.Equal(t, ".png", m.Extension())
|
assert.Equal(t, ".png", m.Extension())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,8 @@ func NewThumbs(conf *config.Config) *Thumbs {
|
||||||
return &Thumbs{conf: conf}
|
return &Thumbs{conf: conf}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start creates thumbnail images for all files found in the originals and sidecar folders.
|
// Start creates thumbnails for files in the originals and sidecar folders.
|
||||||
func (w *Thumbs) Start(force, originalsOnly bool) (err error) {
|
func (w *Thumbs) Start(dir string, force, originalsOnly bool) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = fmt.Errorf("thumbs: %s (panic)\nstack: %s", r, debug.Stack())
|
err = fmt.Errorf("thumbs: %s (panic)\nstack: %s", r, debug.Stack())
|
||||||
|
@ -36,13 +36,22 @@ func (w *Thumbs) Start(force, originalsOnly bool) (err error) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
originalsPath := w.conf.OriginalsPath()
|
originalsPath := w.conf.OriginalsPath()
|
||||||
|
originalsDir := filepath.Join(originalsPath, dir)
|
||||||
sidecarPath := w.conf.SidecarPath()
|
sidecarPath := w.conf.SidecarPath()
|
||||||
|
sidecarDir := filepath.Join(sidecarPath, dir)
|
||||||
|
|
||||||
originalsOnly = originalsOnly || sidecarPath == "" || sidecarPath == originalsPath
|
// Valid path provided?
|
||||||
|
if !fs.PathExists(originalsDir) {
|
||||||
|
return fmt.Errorf("thumbs: directory %s not found", clean.Log(originalsDir))
|
||||||
|
}
|
||||||
|
|
||||||
if _, err = w.Dir(originalsPath, force); err != nil || originalsOnly {
|
// Scan sidecar folder?
|
||||||
|
originalsOnly = originalsOnly || sidecarPath == "" || sidecarPath == originalsPath || !fs.PathExists(sidecarDir)
|
||||||
|
|
||||||
|
// Start creating thumbnails.
|
||||||
|
if _, err = w.Dir(originalsDir, force); err != nil || originalsOnly {
|
||||||
return err
|
return err
|
||||||
} else if _, err = w.Dir(sidecarPath, force); err != nil {
|
} else if _, err = w.Dir(sidecarDir, force); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,10 +59,10 @@ func (w *Thumbs) Start(force, originalsOnly bool) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dir creates thumbnail images for files found in a given path.
|
// Dir creates thumbnail images for files found in a given path.
|
||||||
func (w *Thumbs) Dir(dir string, force bool) (done fs.Done, err error) {
|
func (w *Thumbs) Dir(dir string, force bool) (fs.Done, error) {
|
||||||
done = make(fs.Done)
|
done := make(fs.Done)
|
||||||
|
|
||||||
if err = mutex.MainWorker.Start(); err != nil {
|
if err := mutex.MainWorker.Start(); err != nil {
|
||||||
return done, err
|
return done, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,18 +132,18 @@ func (w *Thumbs) Dir(dir string, force bool) (done fs.Done, err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("thumbs: processing files in %s folder", clean.Log(filepath.Base(dir)))
|
log.Infof("thumbs: processing %s", clean.Log(dir))
|
||||||
|
|
||||||
if err := ignore.Dir(dir); err != nil {
|
if err := ignore.Dir(dir); err != nil {
|
||||||
log.Infof("thumbs: %s", err)
|
log.Infof("thumbs: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = godirwalk.Walk(dir, &godirwalk.Options{
|
err := godirwalk.Walk(dir, &godirwalk.Options{
|
||||||
ErrorCallback: func(fileName string, err error) godirwalk.ErrorAction {
|
ErrorCallback: func(fileName string, err error) godirwalk.ErrorAction {
|
||||||
return godirwalk.SkipNode
|
return godirwalk.SkipNode
|
||||||
},
|
},
|
||||||
Callback: handler,
|
Callback: handler,
|
||||||
Unsorted: true,
|
Unsorted: false,
|
||||||
FollowSymbolicLinks: true,
|
FollowSymbolicLinks: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ func TestResample_Start(t *testing.T) {
|
||||||
|
|
||||||
rs := NewThumbs(conf)
|
rs := NewThumbs(conf)
|
||||||
|
|
||||||
err := rs.Start(true, false)
|
err := rs.Start("", true, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/media"
|
"github.com/photoprism/photoprism/pkg/media"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FoldersByPath returns a slice of folders in a given directory incl sub-folders in recursive mode.
|
// FoldersByPath returns a slice of folders in a given directory incl subfolders in recursive mode.
|
||||||
func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders entity.Folders, err error) {
|
func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders entity.Folders, err error) {
|
||||||
dirs, err := fs.Dirs(filepath.Join(rootPath, path), recursive, true)
|
dirs, err := fs.Dirs(filepath.Join(rootPath, path), recursive, true)
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,10 @@ const (
|
||||||
|
|
||||||
// MimeType returns the mime type of a file, or an empty string if it could not be detected.
|
// MimeType returns the mime type of a file, or an empty string if it could not be detected.
|
||||||
func MimeType(filename string) (mimeType string) {
|
func MimeType(filename string) (mimeType string) {
|
||||||
|
if filename == "" {
|
||||||
|
return MimeTypeUnknown
|
||||||
|
}
|
||||||
|
|
||||||
// Workaround for types that cannot be reliably detected.
|
// Workaround for types that cannot be reliably detected.
|
||||||
switch Extensions[strings.ToLower(filepath.Ext(filename))] {
|
switch Extensions[strings.ToLower(filepath.Ext(filename))] {
|
||||||
case ImageDNG:
|
case ImageDNG:
|
||||||
|
|
Loading…
Reference in a new issue