People: Improve logging, command help, and handling of broken files #22
This commit is contained in:
parent
00ced8137b
commit
f7153cdd21
876
frontend/package-lock.json
generated
876
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -41,13 +41,13 @@
|
|||
@click.stop.prevent="downloadFile(file)">
|
||||
<translate>Download</translate>
|
||||
</v-btn>
|
||||
<v-btn v-if="features.edit && file.Type === 'jpg' && !file.Primary" small depressed dark
|
||||
<v-btn v-if="features.edit && file.Type === 'jpg' && file.Error === '' && !file.Primary" small depressed dark
|
||||
color="primary-button"
|
||||
class="ma-0 action-primary"
|
||||
@click.stop.prevent="primaryFile(file)">
|
||||
<translate>Primary</translate>
|
||||
</v-btn>
|
||||
<v-btn v-if="features.edit && !file.Sidecar && !file.Primary && file.Root === '/'" small
|
||||
<v-btn v-if="features.edit && !file.Sidecar && file.Error === '' && !file.Primary && file.Root === '/'" small
|
||||
depressed dark color="primary-button"
|
||||
class="ma-0 action-unstack"
|
||||
@click.stop.prevent="unstackFile(file)">
|
||||
|
@ -66,6 +66,12 @@
|
|||
</td>
|
||||
<td>{{ file.UID | uppercase }}</td>
|
||||
</tr>
|
||||
<tr v-if="file.Error">
|
||||
<td>
|
||||
<translate>Error</translate>
|
||||
</td>
|
||||
<td><span class="body-2">{{ file.Error | uppercase }}</span></td>
|
||||
</tr>
|
||||
<tr v-if="file.InstanceID" title="XMP">
|
||||
<td>
|
||||
<translate>Instance ID</translate>
|
||||
|
@ -162,12 +168,6 @@
|
|||
</td>
|
||||
<td>{{ file.Chroma }} / 100</td>
|
||||
</tr>
|
||||
<tr v-if="file.Error">
|
||||
<td>
|
||||
<translate>Error</translate>
|
||||
</td>
|
||||
<td>{{ file.Error }}</td>
|
||||
</tr>
|
||||
<tr v-if="file.Missing">
|
||||
<td>
|
||||
<translate>Missing</translate>
|
||||
|
|
|
@ -76,7 +76,7 @@ func AlbumCover(router *gin.RouterGroup) {
|
|||
f, err := query.AlbumCoverByUID(uid)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("%s: no photos yet, using generic image for %s", albumCover, uid)
|
||||
log.Debugf("%s: %s contains no photos, using generic cover", albumCover, uid)
|
||||
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
|
||||
return
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ func AlbumCover(router *gin.RouterGroup) {
|
|||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
||||
|
||||
if !fs.FileExists(fileName) {
|
||||
log.Errorf("%s: could not find original for %s", albumCover, fileName)
|
||||
log.Errorf("%s: found no original for %s", albumCover, fileName)
|
||||
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore.
|
||||
|
|
|
@ -15,7 +15,7 @@ func TestAlbumCover(t *testing.T) {
|
|||
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("album has no photo (because is not existing)", func(t *testing.T) {
|
||||
t.Run("album contains no photos (because is not existing)", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
AlbumCover(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/albums/987-986435/t/"+conf.PreviewToken()+"/tile_500")
|
||||
|
|
|
@ -85,7 +85,7 @@ func FolderCover(router *gin.RouterGroup) {
|
|||
f, err := query.FolderCoverByUID(uid)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("%s: no photos yet, using generic image for %s", folderCover, uid)
|
||||
log.Debugf("%s: %s contains no photos, using generic cover", folderCover, uid)
|
||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ func StartImport(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
if len(f.Albums) > 0 {
|
||||
log.Debugf("import: files will be added to album %s", strings.Join(f.Albums, " and "))
|
||||
log.Debugf("import: adding files to album %s", strings.Join(f.Albums, " and "))
|
||||
opt.Albums = f.Albums
|
||||
}
|
||||
|
||||
|
|
|
@ -81,11 +81,11 @@ func PhotoUnstack(router *gin.RouterGroup) {
|
|||
AbortEntityNotFound(c)
|
||||
return
|
||||
} else if related.Len() == 0 {
|
||||
log.Errorf("photo: no files found for %s (unstack)", txt.Quote(baseName))
|
||||
log.Errorf("photo: found no files for %s (unstack)", txt.Quote(baseName))
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
} else if related.Main == nil {
|
||||
log.Errorf("photo: no main file found for %s (unstack)", txt.Quote(baseName))
|
||||
log.Errorf("photo: found no main file for %s (unstack)", txt.Quote(baseName))
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -85,9 +85,9 @@ func configAction(ctx *cli.Context) error {
|
|||
fmt.Printf("%-25s %s\n", "site-url", conf.SiteUrl())
|
||||
fmt.Printf("%-25s %s\n", "site-preview", conf.SitePreview())
|
||||
fmt.Printf("%-25s %s\n", "site-title", conf.SiteTitle())
|
||||
fmt.Printf("%-25s %s\n", "site-author", conf.SiteAuthor())
|
||||
fmt.Printf("%-25s %s\n", "site-caption", conf.SiteCaption())
|
||||
fmt.Printf("%-25s %s\n", "site-description", conf.SiteDescription())
|
||||
fmt.Printf("%-25s %s\n", "site-author", conf.SiteAuthor())
|
||||
fmt.Printf("%-25s %s\n", "cdn-url", conf.CdnUrl("/"))
|
||||
fmt.Printf("%-25s %s\n", "content-uri", conf.ContentUri())
|
||||
fmt.Printf("%-25s %s\n", "static-uri", conf.StaticUri())
|
||||
|
|
|
@ -35,7 +35,7 @@ var FacesCommand = cli.Command{
|
|||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "fix, f",
|
||||
Usage: "issues will be fixed automatically",
|
||||
Usage: "automatically fixes issues",
|
||||
},
|
||||
},
|
||||
Action: facesAuditAction,
|
||||
|
@ -46,7 +46,7 @@ var FacesCommand = cli.Command{
|
|||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "force, f",
|
||||
Usage: "remove all people and faces",
|
||||
Usage: "removes all people and faces",
|
||||
},
|
||||
},
|
||||
Action: facesResetAction,
|
||||
|
@ -63,7 +63,7 @@ var FacesCommand = cli.Command{
|
|||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "force, f",
|
||||
Usage: "update all faces",
|
||||
Usage: "updates all faces",
|
||||
},
|
||||
},
|
||||
Action: facesUpdateAction,
|
||||
|
|
|
@ -48,9 +48,7 @@ func TestIndexCommand(t *testing.T) {
|
|||
// Expected index command output.
|
||||
assert.Contains(t, output, "indexing originals")
|
||||
assert.Contains(t, output, "classify: loading labels")
|
||||
assert.Contains(t, output, "index: no .ppignore file found")
|
||||
assert.Contains(t, output, "index: updating primary files")
|
||||
assert.Contains(t, output, "index: flagging hidden files")
|
||||
assert.Contains(t, output, "index: found no .ppignore file")
|
||||
} else {
|
||||
t.Fatal("log output missing")
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ func resetAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
removeSidecarJsonPrompt := promptui.Prompt{
|
||||
Label: "Permanently delete all *.json photo sidecar files?",
|
||||
Label: "Permanently remove all JSON photo sidecar files?",
|
||||
IsConfirm: true,
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ func resetAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
if len(matches) > 0 {
|
||||
log.Infof("%d json photo sidecar files will be removed", len(matches))
|
||||
log.Infof("removing %d JSON photo sidecar files", len(matches))
|
||||
|
||||
for _, name := range matches {
|
||||
if err := os.Remove(name); err != nil {
|
||||
|
@ -89,16 +89,16 @@ func resetAction(ctx *cli.Context) error {
|
|||
|
||||
fmt.Println("")
|
||||
|
||||
log.Infof("removed json files [%s]", time.Since(start))
|
||||
log.Infof("removed JSON sidecar files [%s]", time.Since(start))
|
||||
} else {
|
||||
log.Infof("no json files found")
|
||||
log.Infof("found no JSON sidecar files")
|
||||
}
|
||||
} else {
|
||||
log.Infof("keeping json sidecar files")
|
||||
log.Infof("keeping JSON sidecar files")
|
||||
}
|
||||
|
||||
removeSidecarYamlPrompt := promptui.Prompt{
|
||||
Label: "Permanently delete all *.yml photo metadata backups?",
|
||||
Label: "Permanently remove all YAML photo metadata backup files?",
|
||||
IsConfirm: true,
|
||||
}
|
||||
|
||||
|
@ -112,7 +112,7 @@ func resetAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
if len(matches) > 0 {
|
||||
log.Infof("%d photo metadata backups will be removed", len(matches))
|
||||
log.Infof("%d YAML photo metadata backup files will be removed", len(matches))
|
||||
|
||||
for _, name := range matches {
|
||||
if err := os.Remove(name); err != nil {
|
||||
|
@ -124,16 +124,16 @@ func resetAction(ctx *cli.Context) error {
|
|||
|
||||
fmt.Println("")
|
||||
|
||||
log.Infof("removed files [%s]", time.Since(start))
|
||||
log.Infof("removed YAML photo metadata backup files [%s]", time.Since(start))
|
||||
} else {
|
||||
log.Infof("no metadata backups found for removal")
|
||||
log.Infof("found no YAML photo metadata backup files")
|
||||
}
|
||||
} else {
|
||||
log.Infof("keeping backup files")
|
||||
log.Infof("keeping YAML photo metadata backup files")
|
||||
}
|
||||
|
||||
removeAlbumYamlPrompt := promptui.Prompt{
|
||||
Label: "Permanently delete all *.yml album backups?",
|
||||
Label: "Permanently remove all YAML album backup files?",
|
||||
IsConfirm: true,
|
||||
}
|
||||
|
||||
|
@ -147,7 +147,7 @@ func resetAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
if len(matches) > 0 {
|
||||
log.Infof("%d album backups will be removed", len(matches))
|
||||
log.Infof("%d YAML album backups will be removed", len(matches))
|
||||
|
||||
for _, name := range matches {
|
||||
if err := os.Remove(name); err != nil {
|
||||
|
@ -159,12 +159,12 @@ func resetAction(ctx *cli.Context) error {
|
|||
|
||||
fmt.Println("")
|
||||
|
||||
log.Infof("removed files [%s]", time.Since(start))
|
||||
log.Infof("removed YAML album backup files [%s]", time.Since(start))
|
||||
} else {
|
||||
log.Infof("no album backups found for removal")
|
||||
log.Infof("found no YAML album backup files")
|
||||
}
|
||||
} else {
|
||||
log.Infof("keeping backup files")
|
||||
log.Infof("keeping YAML album backup files")
|
||||
}
|
||||
|
||||
conf.Shutdown()
|
||||
|
|
|
@ -42,6 +42,9 @@ var TotalMem uint64
|
|||
|
||||
const ApiUri = "/api/v1"
|
||||
const StaticUri = "/static"
|
||||
const DefaultWakeupInterval = int(15 * 60)
|
||||
const DefaultAutoIndexDelay = int(5 * 60)
|
||||
const DefaultAutoImportDelay = int(3 * 60)
|
||||
|
||||
// Megabyte in bytes.
|
||||
const Megabyte = 1000 * 1000
|
||||
|
@ -328,6 +331,11 @@ func (c *Config) SiteTitle() string {
|
|||
return c.options.SiteTitle
|
||||
}
|
||||
|
||||
// SiteAuthor returns the site author / copyright.
|
||||
func (c *Config) SiteAuthor() string {
|
||||
return c.options.SiteAuthor
|
||||
}
|
||||
|
||||
// SiteCaption returns a short site caption.
|
||||
func (c *Config) SiteCaption() string {
|
||||
return c.options.SiteCaption
|
||||
|
@ -338,11 +346,6 @@ func (c *Config) SiteDescription() string {
|
|||
return c.options.SiteDescription
|
||||
}
|
||||
|
||||
// SiteAuthor returns the site author / copyright.
|
||||
func (c *Config) SiteAuthor() string {
|
||||
return c.options.SiteAuthor
|
||||
}
|
||||
|
||||
// Debug tests if debug mode is enabled.
|
||||
func (c *Config) Debug() bool {
|
||||
return c.options.Debug
|
||||
|
@ -372,7 +375,7 @@ func (c *Config) Public() bool {
|
|||
return c.options.Public
|
||||
}
|
||||
|
||||
// Modify Public state while running. For testing purposes only.
|
||||
// SetPublic changes authentication while instance is running, for testing purposes only.
|
||||
func (c *Config) SetPublic(p bool) {
|
||||
if c.Debug() {
|
||||
c.options.Public = p
|
||||
|
@ -472,7 +475,7 @@ func (c *Config) Workers() int {
|
|||
// WakeupInterval returns the background worker wakeup interval duration.
|
||||
func (c *Config) WakeupInterval() time.Duration {
|
||||
if c.options.WakeupInterval <= 0 || c.options.WakeupInterval > 86400 {
|
||||
return 15 * time.Minute
|
||||
return time.Duration(DefaultWakeupInterval) * time.Second
|
||||
}
|
||||
|
||||
return time.Duration(c.options.WakeupInterval) * time.Second
|
||||
|
@ -483,7 +486,7 @@ func (c *Config) AutoIndex() time.Duration {
|
|||
if c.options.AutoIndex < 0 {
|
||||
return time.Duration(0)
|
||||
} else if c.options.AutoIndex == 0 || c.options.AutoIndex > 86400 {
|
||||
return 5 * time.Minute
|
||||
return time.Duration(DefaultAutoIndexDelay) * time.Second
|
||||
}
|
||||
|
||||
return time.Duration(c.options.AutoIndex) * time.Second
|
||||
|
@ -494,13 +497,13 @@ func (c *Config) AutoImport() time.Duration {
|
|||
if c.options.AutoImport < 0 || c.ReadOnly() {
|
||||
return time.Duration(0)
|
||||
} else if c.options.AutoImport == 0 || c.options.AutoImport > 86400 {
|
||||
return 3 * time.Minute
|
||||
return time.Duration(DefaultAutoImportDelay) * time.Second
|
||||
}
|
||||
|
||||
return time.Duration(c.options.AutoImport) * time.Second
|
||||
}
|
||||
|
||||
// GeoApi returns the preferred geo coding api (none or places).
|
||||
// GeoApi returns the preferred geocoding api (none or places).
|
||||
func (c *Config) GeoApi() string {
|
||||
if c.options.DisablePlaces {
|
||||
return ""
|
||||
|
|
|
@ -11,18 +11,18 @@ import (
|
|||
var GlobalFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "run in debug mode, shows additional log messages",
|
||||
Usage: "enables the debug mode, shows additional log messages",
|
||||
EnvVar: "PHOTOPRISM_DEBUG",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "test",
|
||||
Hidden: true,
|
||||
Usage: "run in test mode",
|
||||
Usage: "enables the test mode",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "demo",
|
||||
Hidden: true,
|
||||
Usage: "run in demo mode",
|
||||
Usage: "enables the demo mode",
|
||||
EnvVar: "PHOTOPRISM_DEMO",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
|
@ -33,99 +33,102 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.BoolFlag{
|
||||
Name: "public, p",
|
||||
Usage: "no authentication required, disables password protection",
|
||||
Usage: "disables password authentication",
|
||||
EnvVar: "PHOTOPRISM_PUBLIC",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "read-only, r",
|
||||
Usage: "don't modify originals folder; disables import, upload, and delete",
|
||||
Usage: "disables import, upload, and delete",
|
||||
EnvVar: "PHOTOPRISM_READONLY",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "experimental, e",
|
||||
Usage: "enable experimental features",
|
||||
Usage: "enables experimental features",
|
||||
EnvVar: "PHOTOPRISM_EXPERIMENTAL",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "admin-password",
|
||||
Usage: "initial admin `PASSWORD`, min 4 characters",
|
||||
Usage: "sets the initial admin `PASSWORD`, min 4 characters",
|
||||
EnvVar: "PHOTOPRISM_ADMIN_PASSWORD",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "config-file, c",
|
||||
Usage: "load initial config options from `FILENAME`",
|
||||
Usage: "loads config options from `FILENAME`",
|
||||
EnvVar: "PHOTOPRISM_CONFIG_FILE",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "config-path",
|
||||
Usage: "config `PATH` containing application settings",
|
||||
Usage: "sets the config `PATH` for storing settings and other config files",
|
||||
EnvVar: "PHOTOPRISM_CONFIG_PATH",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "originals-path",
|
||||
Usage: "originals `PATH` containing your photo and video collection",
|
||||
Usage: "sets the originals `PATH` containing your photo and video files",
|
||||
EnvVar: "PHOTOPRISM_ORIGINALS_PATH",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "originals-limit",
|
||||
Value: 1000,
|
||||
Usage: "file size limit for originals in `MB`",
|
||||
Usage: "limits the file size in `MB`",
|
||||
EnvVar: "PHOTOPRISM_ORIGINALS_LIMIT",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "import-path",
|
||||
Usage: "optional `PATH` for importing files to originals",
|
||||
Usage: "sets an optional import `PATH` from which files can be added to originals",
|
||||
EnvVar: "PHOTOPRISM_IMPORT_PATH",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "storage-path",
|
||||
Usage: "storage `PATH` for cache, database and sidecar files",
|
||||
Usage: "sets the storage base `PATH` for cache, database, and sidecar files",
|
||||
EnvVar: "PHOTOPRISM_STORAGE_PATH",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "sidecar-path",
|
||||
Usage: "relative or absolute storage `PATH` for sidecar files",
|
||||
Usage: "sets a custom relative or absolute sidecar `PATH`",
|
||||
EnvVar: "PHOTOPRISM_SIDECAR_PATH",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "cache-path",
|
||||
Usage: "cache storage `PATH` for sessions and thumbnails",
|
||||
Usage: "sets a custom cache `PATH` for storing sessions and thumbnails",
|
||||
EnvVar: "PHOTOPRISM_CACHE_PATH",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "temp-path",
|
||||
Usage: "temporary `PATH` for storing uploads and downloads",
|
||||
Usage: "sets a custom temp `PATH` for storing temporary files",
|
||||
EnvVar: "PHOTOPRISM_TEMP_PATH",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "backup-path",
|
||||
Usage: "backup storage `PATH`",
|
||||
Usage: "sets a custom backup `PATH`",
|
||||
EnvVar: "PHOTOPRISM_BACKUP_PATH",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "assets-path",
|
||||
Usage: "assets `PATH` for static resources like models and templates",
|
||||
Usage: "sets the assets `PATH` containing static resources like icons and templates",
|
||||
EnvVar: "PHOTOPRISM_ASSETS_PATH",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "workers, w",
|
||||
Usage: "limits `NUMBER` of indexing workers",
|
||||
Usage: "limits the `NUMBER` of indexing workers",
|
||||
EnvVar: "PHOTOPRISM_WORKERS",
|
||||
Value: cpuid.CPU.PhysicalCores / 2,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "wakeup-interval",
|
||||
Usage: "background worker wakeup interval in `SECONDS`",
|
||||
Usage: "adjusts the background worker wakeup interval in `SECONDS`",
|
||||
Value: DefaultWakeupInterval,
|
||||
EnvVar: "PHOTOPRISM_WAKEUP_INTERVAL",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "auto-index",
|
||||
Usage: "auto indexing safety delay in `SECONDS` (WebDAV)",
|
||||
Usage: "adjusts the WebDAV auto indexing safety delay in `SECONDS` (-1 to disable)",
|
||||
Value: DefaultAutoIndexDelay,
|
||||
EnvVar: "PHOTOPRISM_AUTO_INDEX",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "auto-import",
|
||||
Usage: "auto importing safety delay in `SECONDS` (WebDAV)",
|
||||
Usage: "adjusts the WebDAV auto import safety delay in `SECONDS` (-1 to disable)",
|
||||
Value: DefaultAutoImportDelay,
|
||||
EnvVar: "PHOTOPRISM_AUTO_IMPORT",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
|
@ -135,12 +138,12 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.BoolFlag{
|
||||
Name: "disable-webdav",
|
||||
Usage: "disables built-in WebDAV server",
|
||||
Usage: "disables the built-in WebDAV server",
|
||||
EnvVar: "PHOTOPRISM_DISABLE_WEBDAV",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "disable-settings",
|
||||
Usage: "disables settings UI and API",
|
||||
Usage: "disables the settings UI and API",
|
||||
EnvVar: "PHOTOPRISM_DISABLE_SETTINGS",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
|
@ -195,83 +198,83 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.BoolFlag{
|
||||
Name: "detect-nsfw",
|
||||
Usage: "flag photos as private that may be offensive (requires TensorFlow)",
|
||||
Usage: "flags photos as private that may be offensive (requires TensorFlow)",
|
||||
EnvVar: "PHOTOPRISM_DETECT_NSFW",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "upload-nsfw",
|
||||
Usage: "allow uploads that may be offensive",
|
||||
Usage: "allows uploads that may be offensive",
|
||||
EnvVar: "PHOTOPRISM_UPLOAD_NSFW",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "log-level, l",
|
||||
Usage: "trace, debug, info, warning, error, fatal or panic",
|
||||
Usage: "adjusts the log level to trace, debug, info, warning, error, fatal or panic",
|
||||
Value: "info",
|
||||
EnvVar: "PHOTOPRISM_LOG_LEVEL",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "log-filename",
|
||||
Usage: "server log `FILENAME`",
|
||||
Usage: "sets the server log `FILENAME`",
|
||||
EnvVar: "PHOTOPRISM_LOG_FILENAME",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pid-filename",
|
||||
Usage: "server process id `FILENAME`",
|
||||
Usage: "sets the server process id `FILENAME`",
|
||||
EnvVar: "PHOTOPRISM_PID_FILENAME",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "cdn-url",
|
||||
Usage: "content delivery network `URL` (optional)",
|
||||
Usage: "sets an optional content delivery network `URL` (optional)",
|
||||
EnvVar: "PHOTOPRISM_CDN_URL",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "site-url",
|
||||
Usage: "public site `URL`",
|
||||
Usage: "sets the public site `URL`",
|
||||
Value: "http://localhost:2342/",
|
||||
EnvVar: "PHOTOPRISM_SITE_URL",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "site-preview",
|
||||
Usage: "public preview image `URL`",
|
||||
Usage: "sets the public preview image `URL`",
|
||||
EnvVar: "PHOTOPRISM_SITE_PREVIEW",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "site-title",
|
||||
Usage: "site title",
|
||||
Usage: "sets the site title",
|
||||
Value: "PhotoPrism",
|
||||
EnvVar: "PHOTOPRISM_SITE_TITLE",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "site-author",
|
||||
Usage: "sets the artist name or copyright",
|
||||
EnvVar: "PHOTOPRISM_SITE_AUTHOR",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "site-caption",
|
||||
Usage: "short site caption",
|
||||
Usage: "sets the site caption",
|
||||
Value: "Browse Your Life",
|
||||
EnvVar: "PHOTOPRISM_SITE_CAPTION",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "site-description",
|
||||
Usage: "long site description",
|
||||
Usage: "sets an optional site description",
|
||||
EnvVar: "PHOTOPRISM_SITE_DESCRIPTION",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "site-author",
|
||||
Usage: "site artist or copyright",
|
||||
EnvVar: "PHOTOPRISM_SITE_AUTHOR",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "http-port",
|
||||
Value: 2342,
|
||||
Usage: "http server port `NUMBER`",
|
||||
Usage: "sets the http server port `NUMBER`",
|
||||
EnvVar: "PHOTOPRISM_HTTP_PORT",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "http-host",
|
||||
Usage: "http server `IP` address",
|
||||
Usage: "sets the http server `IP` address",
|
||||
EnvVar: "PHOTOPRISM_HTTP_HOST",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "http-mode, m",
|
||||
Usage: "debug, release or test",
|
||||
Usage: "selects the http server `MODE` (debug, release, or test)",
|
||||
EnvVar: "PHOTOPRISM_HTTP_MODE",
|
||||
},
|
||||
cli.StringFlag{
|
||||
|
@ -281,45 +284,45 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.StringFlag{
|
||||
Name: "database-driver",
|
||||
Usage: "database driver `NAME` (sqlite or mysql)",
|
||||
Usage: "selects the database `DRIVER` (sqlite or mysql)",
|
||||
Value: "sqlite",
|
||||
EnvVar: "PHOTOPRISM_DATABASE_DRIVER",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "database-dsn",
|
||||
Usage: "sqlite file name, specifying a `DSN` is optional for mariadb and mysql",
|
||||
Usage: "sets the sqlite file name, specifying a `DSN` is optional other drivers",
|
||||
EnvVar: "PHOTOPRISM_DATABASE_DSN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "database-server",
|
||||
Usage: "database server `HOST`, specifying a :port is optional",
|
||||
Usage: "sets the database server `HOST` and port e.g. mysql:3306",
|
||||
EnvVar: "PHOTOPRISM_DATABASE_SERVER",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "database-name",
|
||||
Value: "photoprism",
|
||||
Usage: "database schema `NAME`",
|
||||
Usage: "sets the database schema `NAME`",
|
||||
EnvVar: "PHOTOPRISM_DATABASE_NAME",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "database-user",
|
||||
Value: "photoprism",
|
||||
Usage: "database user `NAME`",
|
||||
Usage: "sets the database user `NAME`",
|
||||
EnvVar: "PHOTOPRISM_DATABASE_USER",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "database-password",
|
||||
Usage: "database user `PASSWORD`",
|
||||
Usage: "sets the database user `PASSWORD`",
|
||||
EnvVar: "PHOTOPRISM_DATABASE_PASSWORD",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "database-conns",
|
||||
Usage: "`LIMIT` for open database connections",
|
||||
Usage: "limits the `NUMBER` of open database connections",
|
||||
EnvVar: "PHOTOPRISM_DATABASE_CONNS",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "database-conns-idle",
|
||||
Usage: "`LIMIT` for idle database connections",
|
||||
Usage: "limits the `NUMBER` of idle database connections",
|
||||
EnvVar: "PHOTOPRISM_DATABASE_CONNS_IDLE",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
|
@ -329,160 +332,160 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.StringFlag{
|
||||
Name: "darktable-bin",
|
||||
Usage: "Darktable CLI `COMMAND` for RAW file conversion",
|
||||
Usage: "sets the Darktable CLI `COMMAND` for RAW file conversion",
|
||||
Value: "darktable-cli",
|
||||
EnvVar: "PHOTOPRISM_DARKTABLE_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "darktable-blacklist",
|
||||
Usage: "Comma-separated file extension `BLACKLIST`",
|
||||
Usage: "disables using Darktable for specific file `EXTENSIONS`",
|
||||
Value: "raf,cr3,dng",
|
||||
EnvVar: "PHOTOPRISM_DARKTABLE_BLACKLIST",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "rawtherapee-bin",
|
||||
Usage: "RawTherapee CLI `COMMAND` for RAW file conversion",
|
||||
Usage: "sets the RawTherapee CLI `COMMAND` for RAW file conversion",
|
||||
Value: "rawtherapee-cli",
|
||||
EnvVar: "PHOTOPRISM_RAWTHERAPEE_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "rawtherapee-blacklist",
|
||||
Usage: "Comma-separated file extension `BLACKLIST`",
|
||||
Usage: "disables using RawTherapee for specific file `EXTENSIONS`",
|
||||
Value: "",
|
||||
EnvVar: "PHOTOPRISM_RAWTHERAPEE_BLACKLIST",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "sips-bin",
|
||||
Usage: "Sips `COMMAND` for RAW file conversion on macOS",
|
||||
Usage: "sets the Sips `COMMAND` for RAW file conversion on macOS",
|
||||
Value: "sips",
|
||||
EnvVar: "PHOTOPRISM_SIPS_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "heifconvert-bin",
|
||||
Usage: "HEIC/HEIF image convert `COMMAND`",
|
||||
Usage: "sets the HEIC/HEIF image convert `COMMAND`",
|
||||
Value: "heif-convert",
|
||||
EnvVar: "PHOTOPRISM_HEIFCONVERT_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "ffmpeg-bin",
|
||||
Usage: "FFmpeg `COMMAND` for video transcoding and cover images",
|
||||
Usage: "sets the FFmpeg `COMMAND` for video transcoding and cover images",
|
||||
Value: "ffmpeg",
|
||||
EnvVar: "PHOTOPRISM_FFMPEG_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "ffmpeg-encoder",
|
||||
Usage: "FFmpeg AVC encoder `NAME`",
|
||||
Usage: "sets the FFmpeg AVC encoder `NAME`",
|
||||
Value: "libx264",
|
||||
EnvVar: "PHOTOPRISM_FFMPEG_ENCODER",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "ffmpeg-bitrate",
|
||||
Usage: "FFmpeg encoding bitrate `LIMIT` in Mbit/s",
|
||||
Usage: "limits the FFmpeg encoding `BITRATE` (Mbit/s)",
|
||||
Value: 50,
|
||||
EnvVar: "PHOTOPRISM_FFMPEG_BITRATE",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "ffmpeg-buffers",
|
||||
Usage: "FFmpeg capture buffers",
|
||||
Usage: "adjusts the number of FFmpeg capture buffers",
|
||||
Value: 32,
|
||||
EnvVar: "PHOTOPRISM_FFMPEG_BUFFERS",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "exiftool-bin",
|
||||
Usage: "ExifTool `COMMAND` for enhanced metadata extraction",
|
||||
Usage: "sets the ExifTool `COMMAND` for extracting metadata",
|
||||
Value: "exiftool",
|
||||
EnvVar: "PHOTOPRISM_EXIFTOOL_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "download-token",
|
||||
Usage: "static url `TOKEN` for original file downloads",
|
||||
Usage: "sets a custom download url `TOKEN`",
|
||||
EnvVar: "PHOTOPRISM_DOWNLOAD_TOKEN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "preview-token",
|
||||
Usage: "static url `TOKEN` for thumbnails and video streaming",
|
||||
Usage: "sets a custom thumbnail and video streaming url `TOKEN`",
|
||||
EnvVar: "PHOTOPRISM_PREVIEW_TOKEN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "thumb-filter",
|
||||
Usage: "image downscaling `FILTER` (best to worst: blackman, lanczos, cubic, linear)",
|
||||
Usage: "selects the thumbnail downscaling `FILTER` (best to worst: blackman, lanczos, cubic, linear)",
|
||||
Value: "lanczos",
|
||||
EnvVar: "PHOTOPRISM_THUMB_FILTER",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "thumb-size, s",
|
||||
Usage: "max pre-cached thumbnail size in `PIXELS` (720-7680)",
|
||||
Usage: "limits the pre-cached thumbnail size in `PIXELS` (720-7680)",
|
||||
Value: 2048,
|
||||
EnvVar: "PHOTOPRISM_THUMB_SIZE",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "thumb-uncached, u",
|
||||
Usage: "enable on-demand thumbnail generation (high memory and cpu usage)",
|
||||
Usage: "enables on-demand thumbnail generation (high memory and cpu usage)",
|
||||
EnvVar: "PHOTOPRISM_THUMB_UNCACHED",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "thumb-size-uncached, x",
|
||||
Usage: "on-demand thumbnail generation size limit in `PIXELS` (720-7680)",
|
||||
Usage: "limits the on-demand thumbnail size in `PIXELS` (720-7680)",
|
||||
Value: 7680,
|
||||
EnvVar: "PHOTOPRISM_THUMB_SIZE_UNCACHED",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "jpeg-size",
|
||||
Usage: "size limit for converted image files in `PIXELS` (720-30000)",
|
||||
Usage: "limits the size of generated JPEG files in `PIXELS` (720-30000)",
|
||||
Value: 7680,
|
||||
EnvVar: "PHOTOPRISM_JPEG_SIZE",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "jpeg-quality, q",
|
||||
Usage: "set to 90+ for high-quality thumbnails (25-100)",
|
||||
Usage: "adjusts the `QUALITY` of generated JPEG files, a higher value reduces compression (25-100)",
|
||||
Value: 92,
|
||||
EnvVar: "PHOTOPRISM_JPEG_QUALITY",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "face-size",
|
||||
Usage: "face size threshold in `PIXELS`",
|
||||
Usage: "sets the min face size in `PIXELS`",
|
||||
Value: face.SizeThreshold,
|
||||
EnvVar: "PHOTOPRISM_FACE_SIZE",
|
||||
},
|
||||
cli.Float64Flag{
|
||||
Name: "face-score",
|
||||
Usage: "face `QUALITY` threshold",
|
||||
Usage: "adjusts the quality `THRESHOLD` for faces",
|
||||
Value: face.ScoreThreshold,
|
||||
EnvVar: "PHOTOPRISM_FACE_SCORE",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "face-overlap",
|
||||
Usage: "face area overlap threshold in `PERCENT`",
|
||||
Usage: "adjusts the face area overlap threshold in `PERCENT`",
|
||||
Value: face.OverlapThreshold,
|
||||
EnvVar: "PHOTOPRISM_FACE_OVERLAP",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "face-cluster-size",
|
||||
Usage: "size threshold for faces forming a cluster in `PIXELS`",
|
||||
Usage: "sets the min size of faces forming a cluster in `PIXELS`",
|
||||
Value: face.ClusterSizeThreshold,
|
||||
EnvVar: "PHOTOPRISM_FACE_CLUSTER_SIZE",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "face-cluster-score",
|
||||
Usage: "`QUALITY` threshold for faces forming a cluster",
|
||||
Usage: "adjusts the quality `THRESHOLD` for faces forming a cluster",
|
||||
Value: face.ClusterScoreThreshold,
|
||||
EnvVar: "PHOTOPRISM_FACE_CLUSTER_SCORE",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "face-cluster-core",
|
||||
Usage: "`NUMBER` of faces forming a cluster core",
|
||||
Usage: "determines the `NUMBER` of faces forming a cluster core",
|
||||
Value: face.ClusterCore,
|
||||
EnvVar: "PHOTOPRISM_FACE_CLUSTER_CORE",
|
||||
},
|
||||
cli.Float64Flag{
|
||||
Name: "face-cluster-dist",
|
||||
Usage: "similarity `DISTANCE` of faces forming a cluster core",
|
||||
Usage: "determines how close faces forming a cluster core must be to each other",
|
||||
Value: face.ClusterDist,
|
||||
EnvVar: "PHOTOPRISM_FACE_CLUSTER_DIST",
|
||||
},
|
||||
cli.Float64Flag{
|
||||
Name: "face-match-dist",
|
||||
Usage: "`DISTANCE` offset for matching new faces with clusters",
|
||||
Usage: "adjusts the similarity offset for matching faces with existing clusters",
|
||||
Value: face.MatchDist,
|
||||
EnvVar: "PHOTOPRISM_FACE_MATCH_DIST",
|
||||
},
|
||||
|
|
|
@ -78,9 +78,9 @@ type Options struct {
|
|||
SiteUrl string `yaml:"SiteUrl" json:"SiteUrl" flag:"site-url"`
|
||||
SitePreview string `yaml:"SitePreview" json:"SitePreview" flag:"site-preview"`
|
||||
SiteTitle string `yaml:"SiteTitle" json:"SiteTitle" flag:"site-title"`
|
||||
SiteAuthor string `yaml:"SiteAuthor" json:"SiteAuthor" flag:"site-author"`
|
||||
SiteCaption string `yaml:"SiteCaption" json:"SiteCaption" flag:"site-caption"`
|
||||
SiteDescription string `yaml:"SiteDescription" json:"SiteDescription" flag:"site-description"`
|
||||
SiteAuthor string `yaml:"SiteAuthor" json:"SiteAuthor" flag:"site-author"`
|
||||
DatabaseDriver string `yaml:"DatabaseDriver" json:"-" flag:"database-driver"`
|
||||
DatabaseDsn string `yaml:"DatabaseDsn" json:"-" flag:"database-dsn"`
|
||||
DatabaseServer string `yaml:"DatabaseServer" json:"-" flag:"database-server"`
|
||||
|
|
|
@ -161,7 +161,7 @@ func (m *Face) ResolveCollision(embeddings face.Embeddings) (resolved bool, err
|
|||
return false, fmt.Errorf("collision distance must be positive")
|
||||
} else if dist < 0.02 {
|
||||
// Ignore if distance is very small as faces may belong to the same person.
|
||||
log.Infof("faces: %s collision at dist %f reported, same person?", m.ID, dist)
|
||||
log.Warnf("face %s: clearing ambiguous subject %s, similar face at dist %f with source %s", m.ID, m.SubjUID, dist, SrcString(m.FaceSrc))
|
||||
|
||||
// Reset subject UID just in case.
|
||||
m.SubjUID = ""
|
||||
|
@ -182,7 +182,7 @@ func (m *Face) ResolveCollision(embeddings face.Embeddings) (resolved bool, err
|
|||
if revised, err := m.ReviseMatches(); err != nil {
|
||||
return true, err
|
||||
} else if r := len(revised); r > 0 {
|
||||
log.Infof("faces: revised %d matches after collision", r)
|
||||
log.Infof("faces: revised %d matches after conflict", r)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
|
|
@ -209,19 +209,19 @@ func (m *File) DeletePermanently() error {
|
|||
}
|
||||
|
||||
if err := UnscopedDb().Delete(Marker{}, "file_uid = ?", m.FileUID).Error; err != nil {
|
||||
log.Errorf("file: %s (remove markers)", err)
|
||||
log.Errorf("file %s: %s while removing markers", txt.Quote(m.FileUID), err)
|
||||
}
|
||||
|
||||
if err := UnscopedDb().Delete(FileShare{}, "file_id = ?", m.ID).Error; err != nil {
|
||||
log.Errorf("file: %s (remove shares)", err)
|
||||
log.Errorf("file %s: %s while removing share info", txt.Quote(m.FileUID), err)
|
||||
}
|
||||
|
||||
if err := UnscopedDb().Delete(FileSync{}, "file_id = ?", m.ID).Error; err != nil {
|
||||
log.Errorf("file: %s (remove sync)", err)
|
||||
log.Errorf("file %s: %s while removing sync info", txt.Quote(m.FileUID), err)
|
||||
}
|
||||
|
||||
if err := m.ReplaceHash(""); err != nil {
|
||||
log.Errorf("file: %s (remove covers)", err)
|
||||
log.Errorf("file %s: %s while removing covers", txt.Quote(m.FileUID), err)
|
||||
}
|
||||
|
||||
return UnscopedDb().Delete(m).Error
|
||||
|
@ -236,9 +236,9 @@ func (m *File) ReplaceHash(newHash string) error {
|
|||
|
||||
// Log values.
|
||||
if m.FileHash != "" && newHash == "" {
|
||||
log.Tracef("file: removing hash %s", txt.Quote(m.FileHash))
|
||||
log.Tracef("file %s: removing hash %s", txt.Quote(m.FileUID), txt.Quote(m.FileHash))
|
||||
} else if m.FileHash != "" && newHash != "" {
|
||||
log.Tracef("file: hash %s changed to %s", txt.Quote(m.FileHash), txt.Quote(newHash))
|
||||
log.Tracef("file %s: hash %s changed to %s", txt.Quote(m.FileUID), txt.Quote(m.FileHash), txt.Quote(newHash))
|
||||
}
|
||||
|
||||
// Set file hash to new value.
|
||||
|
@ -314,16 +314,16 @@ func (m *File) AllFilesMissing() bool {
|
|||
// Create inserts a new row to the database.
|
||||
func (m *File) Create() error {
|
||||
if m.PhotoID == 0 {
|
||||
return fmt.Errorf("file: photo id must not be empty (create)")
|
||||
return fmt.Errorf("file: can't create file with empty photo id")
|
||||
}
|
||||
|
||||
if err := UnscopedDb().Create(m).Error; err != nil {
|
||||
log.Errorf("file: %s (create)", err)
|
||||
log.Errorf("file: %s while saving", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := m.SaveMarkers(); err != nil {
|
||||
log.Errorf("file: %s (create markers for %s)", err, m.FileUID)
|
||||
log.Errorf("file %s: %s while saving markers", txt.Quote(m.FileUID), err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -342,16 +342,16 @@ func (m *File) ResolvePrimary() error {
|
|||
// Save stores the file in the database.
|
||||
func (m *File) Save() error {
|
||||
if m.PhotoID == 0 {
|
||||
return fmt.Errorf("file: photo id must not be empty (save %s)", m.FileUID)
|
||||
return fmt.Errorf("file %s: can't save file with empty photo id", m.FileUID)
|
||||
}
|
||||
|
||||
if err := UnscopedDb().Save(m).Error; err != nil {
|
||||
log.Errorf("file: %s (save %s)", err, m.FileUID)
|
||||
log.Errorf("file %s: %s while saving", txt.Quote(m.FileUID), err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := m.SaveMarkers(); err != nil {
|
||||
log.Errorf("file: %s (save markers for %s)", err, m.FileUID)
|
||||
log.Errorf("file %s: %s while saving markers", txt.Quote(m.FileUID), err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -381,7 +381,7 @@ func (m *File) Updates(values interface{}) error {
|
|||
|
||||
// Rename updates the name and path of this file.
|
||||
func (m *File) Rename(fileName, rootName, filePath, fileBase string) error {
|
||||
log.Debugf("file: renaming %s to %s", txt.Quote(m.FileName), txt.Quote(fileName))
|
||||
log.Debugf("file %s: renaming %s to %s", txt.Quote(m.FileUID), txt.Quote(m.FileName), txt.Quote(fileName))
|
||||
|
||||
// Update database row.
|
||||
if err := m.Updates(map[string]interface{}{
|
||||
|
@ -425,7 +425,7 @@ func (m *File) Undelete() error {
|
|||
return err
|
||||
}
|
||||
|
||||
log.Debugf("file: removed missing flag from %s", txt.Quote(m.FileName))
|
||||
log.Debugf("file %s: removed missing flag from %s", txt.Quote(m.FileUID), txt.Quote(m.FileName))
|
||||
|
||||
m.FileMissing = false
|
||||
m.DeletedAt = nil
|
||||
|
@ -544,7 +544,7 @@ func (m *File) Markers() *Markers {
|
|||
} else if m.FileUID == "" {
|
||||
m.markers = &Markers{}
|
||||
} else if res, err := FindMarkers(m.FileUID); err != nil {
|
||||
log.Warnf("file: %s (load markers)", err)
|
||||
log.Warnf("file %s: %s while loading markers", txt.Quote(m.FileUID), err)
|
||||
m.markers = &Markers{}
|
||||
} else {
|
||||
m.markers = &res
|
||||
|
|
|
@ -188,7 +188,7 @@ func TestFile_Save(t *testing.T) {
|
|||
t.Fatalf("file id should be 0: %d", file.ID)
|
||||
}
|
||||
|
||||
assert.Equal(t, "file: photo id must not be empty (save 123)", err.Error())
|
||||
assert.Equal(t, "file 123: can't save file with empty photo id", err.Error())
|
||||
})
|
||||
t.Run("success", func(t *testing.T) {
|
||||
photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: "Berlin / Morning Mood"}
|
||||
|
|
|
@ -4,8 +4,11 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/crop"
|
||||
|
@ -69,7 +72,7 @@ func (m *Marker) BeforeCreate(scope *gorm.Scope) error {
|
|||
// NewMarker creates a new entity.
|
||||
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string, size, score int) *Marker {
|
||||
if file.FileHash == "" {
|
||||
log.Errorf("marker: file hash is empty - you might have found a bug")
|
||||
log.Errorf("markers: file hash is empty - you might have found a bug")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -116,6 +119,28 @@ func (m *Marker) SetEmbeddings(e face.Embeddings) {
|
|||
m.EmbeddingsJSON = e.JSON()
|
||||
}
|
||||
|
||||
// UpdateFile sets the file uid and thumb and updates the index if the marker already exists.
|
||||
func (m *Marker) UpdateFile(file *File) (updated bool) {
|
||||
if file.FileUID != "" && m.FileUID != file.FileUID {
|
||||
m.FileUID = file.FileUID
|
||||
updated = true
|
||||
}
|
||||
|
||||
if file.FileHash != "" && !strings.HasPrefix(m.Thumb, file.FileHash) {
|
||||
m.Thumb = crop.NewArea("crop", m.X, m.Y, m.W, m.H).Thumb(file.FileHash)
|
||||
updated = true
|
||||
}
|
||||
|
||||
if !updated || m.MarkerUID == "" {
|
||||
return false
|
||||
} else if res := UnscopedDb().Model(m).UpdateColumns(Values{"file_uid": m.FileUID, "thumb": m.Thumb}); res.Error != nil {
|
||||
log.Errorf("marker %s: %s (set file)", m.MarkerUID, res.Error)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Updates multiple columns in the database.
|
||||
func (m *Marker) Updates(values interface{}) error {
|
||||
return UnscopedDb().Model(m).Updates(values).Error
|
||||
|
@ -207,7 +232,7 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
|
|||
} else if reported, err := f.ResolveCollision(m.Embeddings()); err != nil {
|
||||
return false, err
|
||||
} else if reported {
|
||||
log.Infof("marker: collision reported for %s, face %s, source %s, subject %s <> %s", m.MarkerUID, f.ID, m.SubjSrc, m.SubjUID, f.SubjUID)
|
||||
log.Warnf("marker %s: face %s has ambiguous subjects %s <> %s, subject source %s", txt.Quote(m.MarkerUID), txt.Quote(f.ID), txt.Quote(m.SubjUID), txt.Quote(f.SubjUID), SrcString(m.SubjSrc))
|
||||
return false, nil
|
||||
} else {
|
||||
return false, nil
|
||||
|
@ -329,17 +354,37 @@ func (m *Marker) SyncSubject(updateRelated bool) (err error) {
|
|||
UpdateColumns(Values{"subj_uid": m.SubjUID, "subj_src": SrcAuto, "marker_review": false}).Error; err != nil {
|
||||
return fmt.Errorf("%s (update related markers)", err)
|
||||
} else if res.RowsAffected > 0 && m.face != nil {
|
||||
log.Debugf("marker: matched %s with %s", subj.SubjName, m.FaceID)
|
||||
log.Debugf("markers: matched %s with %s", subj.SubjName, m.FaceID)
|
||||
return m.face.RefreshPhotos()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvalidArea tests if the marker area is invalid or out of range.
|
||||
func (m *Marker) InvalidArea() error {
|
||||
if m.MarkerType != MarkerFace {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ok?
|
||||
if false == (m.X > 1 || m.Y > 1 || m.X < 0 || m.Y < 0 || m.W <= 0 || m.H <= 0 || m.W > 1 || m.H > 1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("invalid %s area x=%d%% y=%d%% w=%d%% h=%d%%", txt.Quote(m.MarkerType), int(m.X*100), int(m.Y*100), int(m.W*100), int(m.H*100))
|
||||
|
||||
if m.MarkerUID == "" {
|
||||
return fmt.Errorf("markers: %s", msg)
|
||||
} else {
|
||||
return fmt.Errorf("marker %s: %s", m.MarkerUID, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Save updates the existing or inserts a new row.
|
||||
func (m *Marker) Save() error {
|
||||
if m.X == 0 || m.Y == 0 || m.X > 1 || m.Y > 1 || m.X < -1 || m.Y < -1 {
|
||||
return fmt.Errorf("marker: invalid position")
|
||||
if err := m.InvalidArea(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Db().Save(m).Error
|
||||
|
@ -347,8 +392,8 @@ func (m *Marker) Save() error {
|
|||
|
||||
// Create inserts a new row to the database.
|
||||
func (m *Marker) Create() error {
|
||||
if m.X == 0 || m.Y == 0 || m.X > 1 || m.Y > 1 || m.X < -1 || m.Y < -1 {
|
||||
return fmt.Errorf("marker: invalid position")
|
||||
if err := m.InvalidArea(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Db().Create(m).Error
|
||||
|
@ -361,7 +406,7 @@ func (m *Marker) Embeddings() face.Embeddings {
|
|||
} else if len(m.embeddings) > 0 {
|
||||
return m.embeddings
|
||||
} else if err := json.Unmarshal(m.EmbeddingsJSON, &m.embeddings); err != nil {
|
||||
log.Errorf("marker: %s while parsing embeddings json", err)
|
||||
log.Errorf("markers: %s while parsing embeddings json", err)
|
||||
}
|
||||
|
||||
return m.embeddings
|
||||
|
@ -389,10 +434,10 @@ func (m *Marker) Subject() (subj *Subject) {
|
|||
// Create subject?
|
||||
if m.SubjSrc != SrcAuto && m.MarkerName != "" && m.SubjUID == "" {
|
||||
if subj = NewSubject(m.MarkerName, SubjPerson, m.SubjSrc); subj == nil {
|
||||
log.Errorf("marker: invalid subject %s", txt.Quote(m.MarkerName))
|
||||
log.Errorf("marker %s: invalid subject %s", txt.Quote(m.MarkerUID), txt.Quote(m.MarkerName))
|
||||
return nil
|
||||
} else if subj = FirstOrCreateSubject(subj); subj == nil {
|
||||
log.Debugf("marker: invalid subject %s", txt.Quote(m.MarkerName))
|
||||
log.Debugf("marker %s: invalid subject %s", txt.Quote(m.MarkerUID), txt.Quote(m.MarkerName))
|
||||
return nil
|
||||
} else {
|
||||
m.subject = subj
|
||||
|
@ -418,9 +463,9 @@ func (m *Marker) ClearSubject(src string) error {
|
|||
// Find and (soft) delete unused subjects.
|
||||
start := time.Now()
|
||||
if count, err := DeleteOrphanPeople(); err != nil {
|
||||
log.Errorf("marker: %s while removing unused subjects [%s]", err, time.Since(start))
|
||||
log.Errorf("marker %s: %s while removing unused subjects [%s]", txt.Quote(m.MarkerUID), err, time.Since(start))
|
||||
} else if count > 0 {
|
||||
log.Debugf("marker: removed %d people [%s]", count, time.Since(start))
|
||||
log.Debugf("marker %s: removed %s [%s]", txt.Quote(m.MarkerUID), english.Plural(count, "person", "people"), time.Since(start))
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -433,7 +478,7 @@ func (m *Marker) ClearSubject(src string) error {
|
|||
} else if resolved, err := m.face.ResolveCollision(m.Embeddings()); err != nil {
|
||||
return err
|
||||
} else if resolved {
|
||||
log.Debugf("marker: resolved collision with face %s", m.face.ID)
|
||||
log.Debugf("marker %s: resolved ambiguous subjects for face %s", txt.Quote(m.MarkerUID), txt.Quote(m.face.ID))
|
||||
}
|
||||
|
||||
// Clear references.
|
||||
|
@ -446,7 +491,7 @@ func (m *Marker) ClearSubject(src string) error {
|
|||
// Face returns a matching face entity if possible.
|
||||
func (m *Marker) Face() (f *Face) {
|
||||
if m.MarkerUID == "" {
|
||||
log.Debugf("marker: empty uid while finding face")
|
||||
log.Debugf("markers: can't find face when uid is empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -459,19 +504,19 @@ func (m *Marker) Face() (f *Face) {
|
|||
// Add face if size
|
||||
if m.SubjSrc != SrcAuto && m.FaceID == "" {
|
||||
if m.Size < face.ClusterSizeThreshold || m.Score < face.ClusterScoreThreshold {
|
||||
log.Debugf("marker: skipped adding face due to low-quality (uid %s, size %d, score %d)", txt.Quote(m.MarkerUID), m.Size, m.Score)
|
||||
log.Debugf("marker %s: skipped adding face due to low-quality (size %d, score %d)", txt.Quote(m.MarkerUID), m.Size, m.Score)
|
||||
return nil
|
||||
} else if emb := m.Embeddings(); emb.Empty() {
|
||||
log.Warnf("marker: %s has no embeddings", m.MarkerUID)
|
||||
log.Warnf("marker %s: found no face embeddings", txt.Quote(m.MarkerUID))
|
||||
return nil
|
||||
} else if f = NewFace(m.SubjUID, m.SubjSrc, emb); f == nil {
|
||||
log.Warnf("marker: failed adding face for id %s", m.MarkerUID)
|
||||
log.Warnf("marker %s: failed assigning face", txt.Quote(m.MarkerUID))
|
||||
return nil
|
||||
} else if f = FirstOrCreateFace(f); f == nil {
|
||||
log.Warnf("marker: failed adding face for id %s", m.MarkerUID)
|
||||
log.Warnf("marker %s: failed assigning face", txt.Quote(m.MarkerUID))
|
||||
return nil
|
||||
} else if err := f.MatchMarkers(Faceless); err != nil {
|
||||
log.Errorf("marker: %s (match faces)", err)
|
||||
log.Errorf("marker %s: %s while matching with faces", txt.Quote(m.MarkerUID), err)
|
||||
}
|
||||
|
||||
m.face = f
|
||||
|
@ -654,30 +699,28 @@ func FindFaceMarker(faceId string) *Marker {
|
|||
if err := Db().Where("face_id = ?", faceId).
|
||||
Where("thumb <> '' AND marker_invalid = 0").
|
||||
Order("face_dist ASC, q DESC").First(&result).Error; err != nil {
|
||||
log.Warnf("marker: face %s not found", txt.Quote(faceId))
|
||||
log.Warnf("markers: found no marker for face %s", txt.Quote(faceId))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// UpdateOrCreateMarker updates a marker in the database or creates a new one if needed.
|
||||
func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
|
||||
// CreateMarkerIfNotExists updates a marker in the database or creates a new one if needed.
|
||||
func CreateMarkerIfNotExists(m *Marker) (*Marker, error) {
|
||||
result := Marker{}
|
||||
|
||||
if m.MarkerUID != "" {
|
||||
err := m.Save()
|
||||
log.Debugf("marker: updated existing %s %s for %s", txt.Quote(m.MarkerType), txt.Quote(m.MarkerUID), txt.Quote(m.FileUID))
|
||||
return m, err
|
||||
return m, nil
|
||||
} else if err := Db().Where(`file_uid = ? AND thumb = ? AND marker_type = ?`,
|
||||
m.FileUID, m.Thumb, m.MarkerType).First(&result).Error; err == nil {
|
||||
log.Infof("marker: found existing %s %s for %s", txt.Quote(m.MarkerType), txt.Quote(result.MarkerUID), txt.Quote(result.FileUID))
|
||||
log.Infof("markers: %s marker %s already exists for %s", txt.Quote(m.MarkerType), txt.Quote(result.MarkerUID), txt.Quote(result.FileUID))
|
||||
return &result, err
|
||||
} else if err := m.Create(); err != nil {
|
||||
log.Warnf("marker: %s while creating %s for %s", err, txt.Quote(m.MarkerType), txt.Quote(m.FileUID))
|
||||
log.Warnf("markers: %s while adding %s", err, txt.Quote(m.MarkerType))
|
||||
return m, err
|
||||
} else {
|
||||
log.Debugf("marker: added %s %s for %s", txt.Quote(m.MarkerType), txt.Quote(m.MarkerUID), txt.Quote(m.FileUID))
|
||||
log.Debugf("markers: added %s marker %s for %s", txt.Quote(m.MarkerType), txt.Quote(m.MarkerUID), txt.Quote(m.FileUID))
|
||||
}
|
||||
|
||||
return m, nil
|
||||
|
|
|
@ -16,6 +16,30 @@ var testArea = crop.Area{
|
|||
H: 0.355556,
|
||||
}
|
||||
|
||||
var invalidArea1 = crop.Area{
|
||||
Name: "face",
|
||||
X: -1,
|
||||
Y: 0.206944,
|
||||
W: 0.355556,
|
||||
H: 0.355556,
|
||||
}
|
||||
|
||||
var invalidArea2 = crop.Area{
|
||||
Name: "face",
|
||||
X: 0.1,
|
||||
Y: 0.206944,
|
||||
W: 0,
|
||||
H: 0.355556,
|
||||
}
|
||||
|
||||
var invalidArea3 = crop.Area{
|
||||
Name: "face",
|
||||
X: 0.1,
|
||||
Y: -0.206944,
|
||||
W: 0.1,
|
||||
H: 0.355556,
|
||||
}
|
||||
|
||||
func TestMarker_TableName(t *testing.T) {
|
||||
m := &Marker{}
|
||||
assert.Contains(t, m.TableName(), "markers")
|
||||
|
@ -127,7 +151,7 @@ func TestUpdateOrCreateMarker(t *testing.T) {
|
|||
assert.Equal(t, SrcImage, m.MarkerSrc)
|
||||
assert.Equal(t, MarkerLabel, m.MarkerType)
|
||||
|
||||
m, err := UpdateOrCreateMarker(m)
|
||||
m, err := CreateMarkerIfNotExists(m)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -146,7 +170,7 @@ func TestUpdateOrCreateMarker(t *testing.T) {
|
|||
func TestMarker_Updates(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
|
||||
m, err := UpdateOrCreateMarker(m)
|
||||
m, err := CreateMarkerIfNotExists(m)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -171,7 +195,7 @@ func TestMarker_Updates(t *testing.T) {
|
|||
func TestMarker_Update(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
|
||||
m, err := UpdateOrCreateMarker(m)
|
||||
m, err := CreateMarkerIfNotExists(m)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -192,11 +216,40 @@ func TestMarker_Update(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestMarker_InvalidArea(t *testing.T) {
|
||||
t.Run("TestArea", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerFace, 100, 65)
|
||||
assert.Nil(t, m.InvalidArea())
|
||||
m.MarkerType = MarkerUnknown
|
||||
assert.Nil(t, m.InvalidArea())
|
||||
})
|
||||
t.Run("InvalidArea1", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), invalidArea1, "lt9k3pw1wowuy3c4", SrcImage, MarkerFace, 100, 65)
|
||||
assert.EqualError(t, m.InvalidArea(), "markers: invalid face area x=-100% y=20% w=35% h=35%")
|
||||
m.MarkerUID = "m345634636"
|
||||
assert.EqualError(t, m.InvalidArea(), "marker m345634636: invalid face area x=-100% y=20% w=35% h=35%")
|
||||
m.MarkerType = MarkerUnknown
|
||||
assert.Nil(t, m.InvalidArea())
|
||||
})
|
||||
t.Run("InvalidArea2", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), invalidArea2, "lt9k3pw1wowuy3c4", SrcImage, MarkerFace, 100, 65)
|
||||
assert.Error(t, m.InvalidArea())
|
||||
m.MarkerType = MarkerUnknown
|
||||
assert.Nil(t, m.InvalidArea())
|
||||
})
|
||||
t.Run("InvalidArea3", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), invalidArea3, "lt9k3pw1wowuy3c4", SrcImage, MarkerFace, 100, 65)
|
||||
assert.Error(t, m.InvalidArea())
|
||||
m.MarkerType = MarkerUnknown
|
||||
assert.Nil(t, m.InvalidArea())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMarker_Save(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
|
||||
|
||||
m, err := UpdateOrCreateMarker(m)
|
||||
m, err := CreateMarkerIfNotExists(m)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -229,9 +282,14 @@ func TestMarker_Save(t *testing.T) {
|
|||
assert.NotEmpty(t, p.Files)
|
||||
})
|
||||
t.Run("invalid position", func(t *testing.T) {
|
||||
m := Marker{X: 0, Y: 0}
|
||||
err := m.Save()
|
||||
assert.Equal(t, "marker: invalid position", err.Error())
|
||||
m := Marker{X: -1, Y: 0, W: 0.2, H: 0.133, MarkerType: MarkerFace}
|
||||
|
||||
if err := m.Save(); err == nil {
|
||||
t.Fatal("error expected")
|
||||
} else {
|
||||
assert.Equal(t, "markers: invalid face area x=-100% y=0% w=20% h=13%", err.Error())
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -357,9 +415,13 @@ func TestMarker_SyncSubject(t *testing.T) {
|
|||
|
||||
func TestMarker_Create(t *testing.T) {
|
||||
t.Run("invalid position", func(t *testing.T) {
|
||||
m := Marker{X: 0, Y: 0}
|
||||
m := Marker{X: 0, Y: 0, MarkerType: MarkerFace}
|
||||
err := m.Create()
|
||||
assert.Equal(t, "marker: invalid position", err.Error())
|
||||
if err == nil {
|
||||
t.Fatal("error expected")
|
||||
} else {
|
||||
assert.Equal(t, "markers: invalid face area x=0% y=0% w=0% h=0%", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/face"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
@ -11,18 +13,20 @@ type Markers []Marker
|
|||
|
||||
// Save stores the markers in the database.
|
||||
func (m Markers) Save(file *File) (count int, err error) {
|
||||
for i := range m {
|
||||
if file != nil {
|
||||
m[i].FileUID = file.FileUID
|
||||
}
|
||||
|
||||
if _, err := UpdateOrCreateMarker(&m[i]); err != nil {
|
||||
log.Errorf("markers: %s (save)", err)
|
||||
}
|
||||
if file == nil {
|
||||
return 0, fmt.Errorf("file required for saving markers")
|
||||
}
|
||||
|
||||
if file == nil {
|
||||
return len(m), nil
|
||||
for i := range m {
|
||||
if m[i].UpdateFile(file) {
|
||||
continue
|
||||
}
|
||||
|
||||
if created, err := CreateMarkerIfNotExists(&m[i]); err != nil {
|
||||
log.Errorf("markers: %s (save)", err)
|
||||
} else {
|
||||
m[i] = *created
|
||||
}
|
||||
}
|
||||
|
||||
return file.UpdatePhotoFaceCount()
|
||||
|
|
|
@ -140,7 +140,7 @@ func SavePhotoForm(model Photo, form form.Photo) error {
|
|||
}
|
||||
|
||||
if !model.HasID() {
|
||||
return errors.New("photo: can't save form, id is empty")
|
||||
return errors.New("can't save form when photo id is missing")
|
||||
}
|
||||
|
||||
model.UpdateDateFields()
|
||||
|
@ -167,7 +167,7 @@ func SavePhotoForm(model Photo, form form.Photo) error {
|
|||
}
|
||||
|
||||
if err := model.SyncKeywordLabels(); err != nil {
|
||||
log.Errorf("photo: %s", err)
|
||||
log.Errorf("photo %s: %s while syncing keywords and labels", model.PhotoUID, err)
|
||||
}
|
||||
|
||||
if err := model.UpdateTitle(model.ClassifyLabels()); err != nil {
|
||||
|
@ -175,7 +175,7 @@ func SavePhotoForm(model Photo, form form.Photo) error {
|
|||
}
|
||||
|
||||
if err := model.IndexKeywords(); err != nil {
|
||||
log.Errorf("photo: %s", err.Error())
|
||||
log.Errorf("photo %s: %s while indexing keywords", model.PhotoUID, err.Error())
|
||||
}
|
||||
|
||||
edited := TimeStamp()
|
||||
|
@ -825,7 +825,7 @@ func (m *Photo) SetCoordinates(lat, lng float32, altitude int, source string) {
|
|||
// SetCamera updates the camera.
|
||||
func (m *Photo) SetCamera(camera *Camera, source string) {
|
||||
if camera == nil {
|
||||
log.Warnf("photo: failed updating camera from source '%s'", source)
|
||||
log.Warnf("photo %s: failed updating camera from source %s", txt.Quote(m.PhotoUID), SrcString(source))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -845,7 +845,7 @@ func (m *Photo) SetCamera(camera *Camera, source string) {
|
|||
// SetLens updates the lens.
|
||||
func (m *Photo) SetLens(lens *Lens, source string) {
|
||||
if lens == nil {
|
||||
log.Warnf("photo: failed updating lens from source '%s'", source)
|
||||
log.Warnf("photo %s: failed updating lens from source %s", txt.Quote(m.PhotoUID), SrcString(source))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1073,12 +1073,12 @@ func (m *Photo) SetPrimary(fileUID string) error {
|
|||
if fileUID != "" {
|
||||
// Do nothing.
|
||||
} else if err := Db().Model(File{}).
|
||||
Where("photo_uid = ? AND file_missing = 0 AND file_type = 'jpg'", m.PhotoUID).
|
||||
Where("photo_uid = ? AND file_type = 'jpg' AND file_missing = 0 AND file_error = ''", m.PhotoUID).
|
||||
Order("file_width DESC").Limit(1).
|
||||
Pluck("file_uid", &files).Error; err != nil {
|
||||
return err
|
||||
} else if len(files) == 0 {
|
||||
return fmt.Errorf("%s has no jpeg", m.PhotoUID)
|
||||
return fmt.Errorf("found no valid jpeg for %s", m.PhotoUID)
|
||||
} else {
|
||||
fileUID = files[0]
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@ import (
|
|||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
|
@ -39,12 +42,13 @@ func (m *Photo) SetTitle(title, source string) {
|
|||
// UpdateTitle updated the photo title based on location and labels.
|
||||
func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
||||
if m.TitleSrc != SrcAuto && m.HasTitle() {
|
||||
return fmt.Errorf("photo: won't update title, %s was modified", m.PhotoUID)
|
||||
return fmt.Errorf("photo %s: keeping %s title %s", txt.Quote(m.PhotoUID), SrcString(m.TitleSrc), txt.Quote(m.PhotoTitle))
|
||||
}
|
||||
|
||||
var names string
|
||||
var knownLocation bool
|
||||
|
||||
start := time.Now()
|
||||
oldTitle := m.PhotoTitle
|
||||
fileTitle := m.FileTitle()
|
||||
|
||||
|
@ -62,7 +66,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||
|
||||
// TODO: User defined title format
|
||||
if names != "" {
|
||||
log.Debugf("photo: using %s to create title for %s", txt.Quote(names), m.PhotoUID)
|
||||
log.Debugf("photo %s: generating title from %s (%s)", txt.Quote(m.PhotoUID), english.Plural(len(people), "person", "people"), txt.Quote(names))
|
||||
|
||||
if l := len([]rune(names)); l > 35 {
|
||||
m.SetTitle(names, SrcAuto)
|
||||
|
@ -76,7 +80,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||
m.SetTitle(fmt.Sprintf("%s / %s / %s", names, loc.City(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
}
|
||||
} else if title := labels.Title(loc.Name()); title != "" {
|
||||
log.Debugf("photo: using label %s to create title for %s", txt.Quote(title), m.PhotoUID)
|
||||
log.Debugf("photo %s: generating title from label %s", txt.Quote(m.PhotoUID), txt.Quote(title))
|
||||
if loc.NoCity() || loc.LongCity() || loc.CityContains(title) {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
|
@ -101,7 +105,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||
knownLocation = true
|
||||
|
||||
if names != "" {
|
||||
log.Debugf("photo: using %s to create title for %s", txt.Quote(names), m.PhotoUID)
|
||||
log.Debugf("photo %s: generating title from %s (%s)", txt.Quote(m.PhotoUID), english.Plural(len(people), "person", "people"), txt.Quote(names))
|
||||
|
||||
if l := len([]rune(names)); l > 35 {
|
||||
m.SetTitle(names, SrcAuto)
|
||||
|
@ -115,7 +119,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||
m.SetTitle(fmt.Sprintf("%s / %s / %s", names, m.Place.City(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
}
|
||||
} else if title := labels.Title(fileTitle); title != "" {
|
||||
log.Debugf("photo: using label %s to create title for %s", txt.Quote(title), m.PhotoUID)
|
||||
log.Debugf("photo %s: generating title from label %s", txt.Quote(m.PhotoUID), txt.Quote(title))
|
||||
if m.Place.NoCity() || m.Place.LongCity() || m.Place.CityContains(title) {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), m.Place.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
|
@ -157,7 +161,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||
}
|
||||
|
||||
if m.PhotoTitle != oldTitle {
|
||||
log.Debugf("photo: changed title of %s to %s", m.PhotoUID, txt.Quote(m.PhotoTitle))
|
||||
log.Debugf("photo %s: changed title to %s [%s]", txt.Quote(m.PhotoUID), txt.Quote(m.PhotoTitle), time.Since(start))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -166,7 +170,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||
// UpdateAndSaveTitle updates the photo title and saves it.
|
||||
func (m *Photo) UpdateAndSaveTitle() error {
|
||||
if !m.HasID() {
|
||||
return fmt.Errorf("photo: can't save to database, id is empty")
|
||||
return fmt.Errorf("can't save photo whithout id")
|
||||
}
|
||||
|
||||
m.PhotoFaces = m.FaceCount()
|
||||
|
|
|
@ -101,7 +101,7 @@ func (m *Subject) Delete() error {
|
|||
subjectMutex.Lock()
|
||||
defer subjectMutex.Unlock()
|
||||
|
||||
log.Infof("subject: deleting %s %s", m.SubjType, txt.Quote(m.SubjName))
|
||||
log.Infof("subject: deleting %s %s", txt.Quote(m.SubjType), txt.Quote(m.SubjName))
|
||||
|
||||
event.EntitiesDeleted("subjects", []string{m.SubjUID})
|
||||
|
||||
|
@ -129,7 +129,7 @@ func (m *Subject) Restore() error {
|
|||
if m.Deleted() {
|
||||
m.DeletedAt = nil
|
||||
|
||||
log.Infof("subject: restoring %s %s", m.SubjType, txt.Quote(m.SubjName))
|
||||
log.Infof("subject: restoring %s %s", txt.Quote(m.SubjType), txt.Quote(m.SubjName))
|
||||
|
||||
event.EntitiesCreated("subjects", []*Subject{m})
|
||||
|
||||
|
@ -167,7 +167,7 @@ func FirstOrCreateSubject(m *Subject) *Subject {
|
|||
if found := FindSubjectByName(m.SubjName); found != nil {
|
||||
return found
|
||||
} else if createErr := m.Create(); createErr == nil {
|
||||
log.Infof("subject: added %s %s", m.SubjType, txt.Quote(m.SubjName))
|
||||
log.Infof("subject: added %s %s", txt.Quote(m.SubjType), txt.Quote(m.SubjName))
|
||||
|
||||
event.EntitiesCreated("subjects", []*Subject{m})
|
||||
|
||||
|
@ -257,7 +257,7 @@ func (m *Subject) UpdateName(name string) (*Subject, error) {
|
|||
if err := m.SetName(name); err != nil {
|
||||
return m, err
|
||||
} else if err := m.Updates(Values{"SubjName": m.SubjName, "SubjSlug": m.SubjSlug}); err == nil {
|
||||
log.Infof("subject: renamed %s %s", m.SubjType, txt.Quote(m.SubjName))
|
||||
log.Infof("subject: renamed %s %s", txt.Quote(m.SubjType), txt.Quote(m.SubjName))
|
||||
|
||||
event.EntitiesUpdated("subjects", []*Subject{m})
|
||||
|
||||
|
|
|
@ -136,7 +136,7 @@ func TestSubject_Restore(t *testing.T) {
|
|||
t.Run("success", func(t *testing.T) {
|
||||
var deleteTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
m := &Subject{DeletedAt: &deleteTime, SubjName: "ToBeRestored"}
|
||||
m := &Subject{DeletedAt: &deleteTime, SubjType: SubjPerson, SubjName: "ToBeRestored"}
|
||||
err := m.Save()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -150,7 +150,7 @@ func TestSubject_Restore(t *testing.T) {
|
|||
assert.False(t, m.Deleted())
|
||||
})
|
||||
t.Run("subject not deleted", func(t *testing.T) {
|
||||
m := &Subject{DeletedAt: nil, SubjName: "NotDeleted1234"}
|
||||
m := &Subject{DeletedAt: nil, SubjType: SubjPerson, SubjName: "NotDeleted1234"}
|
||||
err := m.Restore()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -4,17 +4,17 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/crop"
|
||||
)
|
||||
|
||||
var CropSize = crop.Sizes[crop.Tile160]
|
||||
var MatchDist = 0.46
|
||||
var ClusterDist = 0.64
|
||||
var ClusterCore = 4
|
||||
var ClusterScoreThreshold = 15
|
||||
var ClusterSizeThreshold = 80
|
||||
var SampleThreshold = 2 * ClusterCore
|
||||
var OverlapThreshold = 42
|
||||
var OverlapThresholdFloor = OverlapThreshold - 1
|
||||
var ScoreThreshold = 9.0
|
||||
var SizeThreshold = 40
|
||||
var CropSize = crop.Sizes[crop.Tile160] // Face image crop size for FaceNet.
|
||||
var OverlapThreshold = 42 // Face area overlap threshold in percent.
|
||||
var OverlapThresholdFloor = OverlapThreshold - 1 // Reduced overlap area to avoid rounding inconsistencies.
|
||||
var ScoreThreshold = 9.0 // Min face score.
|
||||
var ClusterScoreThreshold = 15 // Min score for faces forming a cluster.
|
||||
var SizeThreshold = 40 // Min face size in pixels.
|
||||
var ClusterSizeThreshold = 80 // Min size for faces forming a cluster in pixels.
|
||||
var ClusterDist = 0.64 // Similarity distance threshold of faces forming a cluster core.
|
||||
var MatchDist = 0.46 // Distance offset threshold for matching new faces with clusters.
|
||||
var ClusterCore = 4 // Min number of faces forming a cluster core.
|
||||
var SampleThreshold = 2 * ClusterCore // Threshold for automatic clustering to start.
|
||||
|
||||
// QualityThreshold returns the scale adjusted quality score threshold.
|
||||
func QualityThreshold(scale int) (score float32) {
|
||||
|
|
|
@ -39,7 +39,7 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
|
|||
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "no exif header") {
|
||||
return rawExif, fmt.Errorf("metadata: 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") {
|
||||
log.Debugf("metadata: failed parsing %s, starting brute-force search (parse jpeg)", logName)
|
||||
} else {
|
||||
|
@ -62,7 +62,7 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
|
|||
|
||||
if err != nil {
|
||||
if err.Error() == "file does not have EXIF" {
|
||||
return rawExif, fmt.Errorf("metadata: no exif header in %s (parse png)", logName)
|
||||
return rawExif, fmt.Errorf("metadata: found no exif header in %s (parse png)", logName)
|
||||
} else {
|
||||
log.Warnf("metadata: %s in %s (parse png)", err, logName)
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
|
|||
|
||||
if err != nil {
|
||||
if err.Error() == "file does not have EXIF" {
|
||||
return rawExif, fmt.Errorf("metadata: no exif header in %s (parse heic)", logName)
|
||||
return rawExif, fmt.Errorf("metadata: found no exif header in %s (parse heic)", logName)
|
||||
} else {
|
||||
log.Warnf("metadata: %s in %s (parse heic)", err, logName)
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
|
|||
|
||||
if err != nil {
|
||||
if err.Error() == "file does not have EXIF" {
|
||||
return rawExif, fmt.Errorf("metadata: no exif header in %s (parse tiff)", logName)
|
||||
return rawExif, fmt.Errorf("metadata: found no exif header in %s (parse tiff)", logName)
|
||||
} else {
|
||||
log.Warnf("metadata: %s in %s (parse tiff)", err, logName)
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
|
|||
rawExif, err = exif.SearchFileAndExtractExif(fileName)
|
||||
|
||||
if err != nil {
|
||||
return rawExif, fmt.Errorf("metadata: no exif header in %s (search and extract)", logName)
|
||||
return rawExif, fmt.Errorf("metadata: found no exif header in %s (search and extract)", logName)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ func TestExif(t *testing.T) {
|
|||
t.Fatal("err should NOT be nil")
|
||||
}
|
||||
|
||||
assert.Equal(t, "metadata: no exif header in tweethog.png (search and extract)", err.Error())
|
||||
assert.Equal(t, "metadata: found no exif header in tweethog.png (search and extract)", err.Error())
|
||||
})
|
||||
|
||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||
|
@ -223,7 +223,7 @@ func TestExif(t *testing.T) {
|
|||
t.Fatal("err should NOT be nil")
|
||||
}
|
||||
|
||||
assert.Equal(t, "metadata: no exif header in no-exif-data.jpg (search and extract)", err.Error())
|
||||
assert.Equal(t, "metadata: found no exif header in no-exif-data.jpg (search and extract)", err.Error())
|
||||
})
|
||||
|
||||
t.Run("screenshot.png", func(t *testing.T) {
|
||||
|
@ -268,7 +268,7 @@ func TestExif(t *testing.T) {
|
|||
t.Run("gopher-preview.jpg", func(t *testing.T) {
|
||||
_, err := Exif("testdata/gopher-preview.jpg", fs.FormatJpeg)
|
||||
|
||||
assert.EqualError(t, err, "metadata: 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) {
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
|
@ -60,37 +62,37 @@ func (w *Faces) Start(opt FacesOptions) (err error) {
|
|||
} else if removed > 0 {
|
||||
log.Infof("faces: removed %d orphan markers [%s]", removed, time.Since(start))
|
||||
} else {
|
||||
log.Debugf("faces: no orphan markers [%s]", time.Since(start))
|
||||
log.Debugf("faces: found no orphan markers [%s]", time.Since(start))
|
||||
}
|
||||
|
||||
// Repair invalid marker face and subject references.
|
||||
start = time.Now()
|
||||
if removed, err := query.FixMarkerReferences(); err != nil {
|
||||
log.Errorf("faces: %s (fix references)", err)
|
||||
log.Errorf("markers: %s (fix references)", err)
|
||||
} else if removed > 0 {
|
||||
log.Infof("faces: fixed %d marker references [%s]", removed, time.Since(start))
|
||||
log.Infof("markers: fixed %d references [%s]", removed, time.Since(start))
|
||||
} else {
|
||||
log.Debugf("faces: no invalid marker references [%s]", time.Since(start))
|
||||
log.Debugf("markers: found no invalid references [%s]", time.Since(start))
|
||||
}
|
||||
|
||||
// Create known marker subjects if needed.
|
||||
start = time.Now()
|
||||
if affected, err := query.CreateMarkerSubjects(); err != nil {
|
||||
log.Errorf("faces: %s (create subjects)", err)
|
||||
log.Errorf("markers: %s (create subjects)", err)
|
||||
} else if affected > 0 {
|
||||
log.Infof("faces: added %d known marker subjects [%s]", affected, time.Since(start))
|
||||
log.Infof("markers: added %d known subjects [%s]", affected, time.Since(start))
|
||||
} else {
|
||||
log.Debugf("faces: marker subjects already exist [%s]", time.Since(start))
|
||||
log.Debugf("markers: found no missing subjects [%s]", time.Since(start))
|
||||
}
|
||||
|
||||
// Resolve collisions of different subject's faces.
|
||||
start = time.Now()
|
||||
if c, r, err := query.ResolveFaceCollisions(); err != nil {
|
||||
log.Errorf("faces: %s (resolve collisions)", err)
|
||||
log.Errorf("faces: %s (resolve ambiguous subjects)", err)
|
||||
} else if c > 0 {
|
||||
log.Infof("faces: resolved %d / %d collisions [%s]", r, c, time.Since(start))
|
||||
log.Infof("faces: resolved %d / %d ambiguous subjects [%s]", r, c, time.Since(start))
|
||||
} else {
|
||||
log.Debugf("faces: no collisions detected [%s]", time.Since(start))
|
||||
log.Debugf("faces: found no ambiguous subjects [%s]", time.Since(start))
|
||||
}
|
||||
|
||||
// Optimize existing face clusters.
|
||||
|
@ -100,7 +102,7 @@ func (w *Faces) Start(opt FacesOptions) (err error) {
|
|||
} else if res.Merged > 0 {
|
||||
log.Infof("faces: merged %d clusters [%s]", res.Merged, time.Since(start))
|
||||
} else {
|
||||
log.Debugf("faces: no clusters could be merged [%s]", time.Since(start))
|
||||
log.Debugf("faces: found no clusters to be merged [%s]", time.Since(start))
|
||||
}
|
||||
|
||||
var added entity.Faces
|
||||
|
@ -125,9 +127,9 @@ func (w *Faces) Start(opt FacesOptions) (err error) {
|
|||
|
||||
// Log face matching results.
|
||||
if matches.Updated > 0 {
|
||||
log.Infof("faces: updated %d markers, recognized %d faces, %d unknown [%s]", matches.Updated, matches.Recognized, matches.Unknown, time.Since(start))
|
||||
log.Infof("faces: updated %s, recognized %s, %d unknown [%s]", english.Plural(int(matches.Updated), "marker", "markers"), english.Plural(int(matches.Recognized), "face", "faces"), matches.Unknown, time.Since(start))
|
||||
} else {
|
||||
log.Debugf("faces: updated %d markers, recognized %d faces, %d unknown [%s]", matches.Updated, matches.Recognized, matches.Unknown, time.Since(start))
|
||||
log.Debugf("faces: updated %s, recognized %s, %d unknown [%s]", english.Plural(int(matches.Updated), "marker", "markers"), english.Plural(int(matches.Recognized), "face", "faces"), matches.Unknown, time.Since(start))
|
||||
}
|
||||
|
||||
// Remove unused people.
|
||||
|
|
|
@ -74,18 +74,18 @@ func (w *Faces) Audit(fix bool) (err error) {
|
|||
|
||||
r := f1.SampleRadius + face.MatchDist
|
||||
|
||||
log.Infof("face %s: conflict at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius)
|
||||
log.Infof("face %s: ambiguous subject at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius)
|
||||
|
||||
if f1.SubjUID != "" {
|
||||
log.Infof("face %s: subject %s (%s %s)", f1.ID, txt.Quote(subj[f1.SubjUID].SubjName), f1.SubjUID, entity.SrcString(f1.FaceSrc))
|
||||
} else {
|
||||
log.Infof("face %s: no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
|
||||
log.Infof("face %s: has no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
|
||||
}
|
||||
|
||||
if f2.SubjUID != "" {
|
||||
log.Infof("face %s: subject %s (%s %s)", f2.ID, txt.Quote(subj[f2.SubjUID].SubjName), f2.SubjUID, entity.SrcString(f2.FaceSrc))
|
||||
} else {
|
||||
log.Infof("face %s: no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
|
||||
log.Infof("face %s: has no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
|
||||
}
|
||||
|
||||
if !fix {
|
||||
|
@ -93,21 +93,21 @@ func (w *Faces) Audit(fix bool) (err error) {
|
|||
} else if ok, err := f1.ResolveCollision(face.Embeddings{f2.Embedding()}); err != nil {
|
||||
log.Errorf("face %s: %s", f1.ID, err)
|
||||
} else if ok {
|
||||
log.Infof("face %s: collision has been resolved", f1.ID)
|
||||
log.Infof("face %s: ambiguous subject has been resolved", f1.ID)
|
||||
resolved++
|
||||
} else {
|
||||
log.Infof("face %s: collision could not be resolved", f1.ID)
|
||||
log.Infof("face %s: ambiguous subject could not be resolved", f1.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if conflicts == 0 {
|
||||
log.Infof("found no conflicting face clusters")
|
||||
log.Infof("found no ambiguous subjects")
|
||||
} else if !fix {
|
||||
log.Infof("%d conflicting face clusters", conflicts)
|
||||
log.Infof("%s", english.Plural(conflicts, "ambiguous subject", "ambiguous subjects"))
|
||||
} else {
|
||||
log.Infof("%d conflicting face clusters, %d resolved", conflicts, resolved)
|
||||
log.Infof("%s, %d resolved", english.Plural(conflicts, "ambiguous subject", "ambiguous subjects"), resolved)
|
||||
}
|
||||
|
||||
if markers, err := query.MarkersWithSubjectConflict(); err != nil {
|
||||
|
|
|
@ -19,16 +19,16 @@ func (w *Faces) Cluster(opt FacesOptions) (added entity.Faces, err error) {
|
|||
|
||||
// Skip clustering if index contains no new face markers, and force option isn't set.
|
||||
if opt.Force {
|
||||
log.Infof("faces: forced clustering")
|
||||
log.Infof("faces: enforced clustering")
|
||||
} else if n := query.CountNewFaceMarkers(face.ClusterSizeThreshold, face.ClusterScoreThreshold); n < opt.SampleThreshold() {
|
||||
log.Debugf("faces: skipping clustering")
|
||||
log.Debugf("faces: skipped clustering")
|
||||
return added, nil
|
||||
}
|
||||
|
||||
// Fetch unclustered face embeddings.
|
||||
embeddings, err := query.Embeddings(false, true, face.ClusterSizeThreshold, face.ClusterScoreThreshold)
|
||||
|
||||
log.Debugf("faces: %d unclustered samples found", len(embeddings))
|
||||
log.Debugf("faces: found %s", english.Plural(len(embeddings), "unclustered sample", "unclustered samples"))
|
||||
|
||||
// Anything that keeps us from doing this?
|
||||
if err != nil {
|
||||
|
@ -75,7 +75,7 @@ func (w *Faces) Cluster(opt FacesOptions) (added entity.Faces, err error) {
|
|||
log.Errorf("faces: face should not be nil - bug?")
|
||||
} else if err := f.Create(); err == nil {
|
||||
added = append(added, *f)
|
||||
log.Debugf("faces: added cluster %s based on %d samples, radius %f", f.ID, f.Samples, f.SampleRadius)
|
||||
log.Debugf("faces: added cluster %s based on %s, radius %f", f.ID, english.Plural(f.Samples, "sample", "samples"), f.SampleRadius)
|
||||
} else if err := f.Updates(entity.Values{"UpdatedAt": entity.TimeStamp()}); err != nil {
|
||||
log.Errorf("faces: %s", err)
|
||||
} else {
|
||||
|
|
|
@ -10,7 +10,7 @@ func (w *Faces) Stats() (err error) {
|
|||
if embeddings, err := query.Embeddings(true, false, 0, 0); err != nil {
|
||||
return err
|
||||
} else if samples := len(embeddings); samples == 0 {
|
||||
log.Infof("faces: no samples found")
|
||||
log.Infof("faces: found no samples")
|
||||
} else {
|
||||
log.Infof("faces: computing distance of %d samples", samples)
|
||||
|
||||
|
@ -101,7 +101,7 @@ func (w *Faces) Stats() (err error) {
|
|||
}
|
||||
|
||||
if l := len(dist); l == 0 {
|
||||
log.Infof("faces: analyzed %d clusters, no matches", samples)
|
||||
log.Infof("faces: analyzed %d clusters, found no matches", samples)
|
||||
} else {
|
||||
log.Infof("faces: %d faces match to the same person", l)
|
||||
}
|
||||
|
|
|
@ -178,7 +178,7 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
continue
|
||||
}
|
||||
} else {
|
||||
log.Warnf("import: no main file for %s, conversion to jpeg failed?", fs.RelName(destMainFileName, imp.originalsPath()))
|
||||
log.Warnf("import: found no main file for %s, conversion to jpeg may have failed", fs.RelName(destMainFileName, imp.originalsPath()))
|
||||
}
|
||||
|
||||
for _, f := range related.Files {
|
||||
|
@ -210,7 +210,7 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
|
||||
if res.Indexed() && f.IsJpeg() {
|
||||
if err := f.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
log.Errorf("import: failed creating thumbs for %s (%s)", txt.Quote(f.BaseName()), err.Error())
|
||||
log.Errorf("import: failed creating thumbnails for %s (%s)", txt.Quote(f.BaseName()), err.Error())
|
||||
query.SetFileError(res.FileUID, err.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,13 +9,13 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/face"
|
||||
|
||||
"github.com/karrick/godirwalk"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/face"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/nsfw"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
|
@ -265,7 +265,7 @@ func (ind *Index) Start(opt IndexOptions) fs.Done {
|
|||
log.Warnf("index: %s (update counts)", err)
|
||||
}
|
||||
} else {
|
||||
log.Infof("index: no new or modified files")
|
||||
log.Infof("index: found no new or modified files")
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
|
|
|
@ -298,8 +298,9 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
case m.IsJpeg():
|
||||
// Color information
|
||||
if p, err := m.Colors(Config().ThumbPath()); err != nil {
|
||||
log.Errorf("index: %s in %s (detect colors)", err.Error(), logName)
|
||||
log.Debugf("%s while detecting colors", err.Error())
|
||||
file.FileError = err.Error()
|
||||
file.FilePrimary = false
|
||||
} else {
|
||||
file.FileMainColor = p.MainColor.Name()
|
||||
file.FileColors = p.Colors.Hex()
|
||||
|
@ -633,9 +634,9 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
details.Keywords = strings.Join(txt.UniqueWords(w), ", ")
|
||||
|
||||
if details.Keywords != "" {
|
||||
log.Tracef("index: set keywords %s for %s", details.Keywords, logName)
|
||||
log.Tracef("index: using keywords %s for %s", details.Keywords, logName)
|
||||
} else {
|
||||
log.Tracef("index: no keywords for %s", logName)
|
||||
log.Tracef("index: found no keywords for %s", logName)
|
||||
}
|
||||
|
||||
photo.PhotoQuality = photo.QualityScore()
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result IndexResult) {
|
||||
// Skip sidecar files without related media file.
|
||||
if related.Main == nil {
|
||||
result.Err = fmt.Errorf("index: no main file found for %s", txt.Quote(related.String()))
|
||||
result.Err = fmt.Errorf("index: found no main file for %s", txt.Quote(related.String()))
|
||||
result.Status = IndexFailed
|
||||
return result
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result Inde
|
|||
log.Debugf("index: created %s", txt.Quote(jpegFile.BaseName()))
|
||||
|
||||
if err := jpegFile.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
result.Err = fmt.Errorf("index: failed creating thumbs for %s (%s)", txt.Quote(f.BaseName()), err.Error())
|
||||
result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", txt.Quote(f.BaseName()), err.Error())
|
||||
result.Status = IndexFailed
|
||||
|
||||
return result
|
||||
|
@ -59,7 +59,7 @@ func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result Inde
|
|||
|
||||
if result.Indexed() && f.IsJpeg() {
|
||||
if err := f.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
log.Errorf("index: failed creating thumbs for %s (%s)", txt.Quote(f.BaseName()), err.Error())
|
||||
log.Errorf("index: failed creating thumbnails for %s (%s)", txt.Quote(f.BaseName()), err.Error())
|
||||
query.SetFileError(result.FileUID, err.Error())
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In
|
|||
log.Debugf("index: created %s", txt.Quote(jpegFile.BaseName()))
|
||||
|
||||
if err := jpegFile.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
result.Err = fmt.Errorf("index: failed creating thumbs for %s (%s)", txt.Quote(f.BaseName()), err.Error())
|
||||
result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", txt.Quote(f.BaseName()), err.Error())
|
||||
result.Status = IndexFailed
|
||||
|
||||
return result
|
||||
|
@ -140,7 +140,7 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In
|
|||
|
||||
if res.Indexed() && f.IsJpeg() {
|
||||
if err := f.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
log.Errorf("index: failed creating thumbs for %s (%s)", txt.Quote(f.BaseName()), err.Error())
|
||||
log.Errorf("index: failed creating thumbnails for %s (%s)", txt.Quote(f.BaseName()), err.Error())
|
||||
query.SetFileError(res.FileUID, err.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ func (m *MediaFile) Location() (*entity.Cell, error) {
|
|||
data := m.MetaData()
|
||||
|
||||
if data.Lat == 0 && data.Lng == 0 {
|
||||
return nil, errors.New("media: no latitude and longitude in metadata")
|
||||
return nil, errors.New("media: found no latitude and longitude")
|
||||
}
|
||||
|
||||
m.location = entity.NewCell(data.Lat, data.Lng)
|
||||
|
|
|
@ -83,7 +83,7 @@ func TestMediaFile_Location(t *testing.T) {
|
|||
if _, err := mediaFile.Location(); err == nil {
|
||||
t.Fatal("mediaFile.Location() should return error")
|
||||
} else {
|
||||
assert.Equal(t, "media: no latitude and longitude in metadata", err.Error())
|
||||
assert.Equal(t, "media: found no latitude and longitude", err.Error())
|
||||
}
|
||||
})
|
||||
t.Run("Random.docx", func(t *testing.T) {
|
||||
|
@ -97,7 +97,7 @@ func TestMediaFile_Location(t *testing.T) {
|
|||
|
||||
location, err := mediaFile.Location()
|
||||
|
||||
assert.Error(t, err, "metadata: no exif header in Random.docx")
|
||||
assert.Error(t, err, "metadata: found no exif header in Random.docx")
|
||||
assert.Nil(t, location)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -902,7 +902,7 @@ func (m *MediaFile) Thumbnail(path string, sizeName thumb.Name) (filename string
|
|||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("media: failed creating thumbnail for %s (%s)", txt.Quote(m.BaseName()), err)
|
||||
log.Error(err)
|
||||
log.Debug(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
@ -928,7 +928,7 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) {
|
|||
defer func() {
|
||||
switch count {
|
||||
case 0:
|
||||
log.Debug(capture.Time(start, fmt.Sprintf("media: no new thumbnails created for %s", m.BasePrefix(false))))
|
||||
log.Debug(capture.Time(start, fmt.Sprintf("media: created no new thumbnails for %s", m.BasePrefix(false))))
|
||||
default:
|
||||
log.Info(capture.Time(start, fmt.Sprintf("media: created %s for %s", english.Plural(count, "thumbnail", "thumbnails"), m.BasePrefix(false))))
|
||||
}
|
||||
|
@ -961,7 +961,7 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) {
|
|||
img, err := imaging.Open(m.FileName())
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("media: %s in %s", err.Error(), txt.Quote(m.BaseName()))
|
||||
log.Debugf("media: %s in %s", err.Error(), txt.Quote(m.BaseName()))
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ func (m *MediaFile) MetaData() (result meta.Data) {
|
|||
// Parse regular JSON sidecar files ("img_1234.json")
|
||||
if !m.IsSidecar() {
|
||||
if jsonFiles := fs.FormatJson.FindAll(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false); len(jsonFiles) == 0 {
|
||||
log.Tracef("metadata: no additional sidecar file found for %s", txt.Quote(filepath.Base(m.FileName())))
|
||||
log.Tracef("metadata: found no additional sidecar file for %s", txt.Quote(filepath.Base(m.FileName())))
|
||||
} else {
|
||||
for _, jsonFile := range jsonFiles {
|
||||
jsonErr := m.metaData.JSON(jsonFile, m.BaseName())
|
||||
|
|
|
@ -84,7 +84,7 @@ func CountNewFaceMarkers(size, score int) (n int) {
|
|||
|
||||
if err := Db().Where("face_src = ?", entity.SrcAuto).
|
||||
Order("created_at DESC").Limit(1).Take(&f).Error; err != nil {
|
||||
log.Debugf("faces: no existing clusters")
|
||||
log.Debugf("faces: found no existing clusters")
|
||||
}
|
||||
|
||||
q := Db().Model(&entity.Markers{}).
|
||||
|
@ -181,27 +181,27 @@ func ResolveFaceCollisions() (conflicts, resolved int, err error) {
|
|||
|
||||
r := f1.SampleRadius + face.MatchDist
|
||||
|
||||
log.Infof("face %s: conflict at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius)
|
||||
log.Infof("face %s: ambiguous subject at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius)
|
||||
|
||||
if f1.SubjUID != "" {
|
||||
log.Debugf("face %s: subject %s (%s %s)", f1.ID, txt.Quote(f1.SubjUID), f1.SubjUID, entity.SrcString(f1.FaceSrc))
|
||||
} else {
|
||||
log.Debugf("face %s: no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
|
||||
log.Debugf("face %s: has no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
|
||||
}
|
||||
|
||||
if f2.SubjUID != "" {
|
||||
log.Debugf("face %s: subject %s (%s %s)", f2.ID, txt.Quote(f2.SubjUID), f2.SubjUID, entity.SrcString(f2.FaceSrc))
|
||||
} else {
|
||||
log.Debugf("face %s: no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
|
||||
log.Debugf("face %s: has no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
|
||||
}
|
||||
|
||||
if ok, err := f1.ResolveCollision(face.Embeddings{f2.Embedding()}); err != nil {
|
||||
log.Errorf("face %s: %s", f1.ID, err)
|
||||
} else if ok {
|
||||
log.Infof("face %s: collision has been resolved", f1.ID)
|
||||
log.Infof("face %s: conflict has been resolved", f1.ID)
|
||||
resolved++
|
||||
} else {
|
||||
log.Debugf("face %s: collision could not be resolved", f1.ID)
|
||||
log.Debugf("face %s: conflict could not be resolved", f1.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package query
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
@ -83,11 +85,19 @@ func PhotosMissing(limit int, offset int) (entities entity.Photos, err error) {
|
|||
|
||||
// ResetPhotoQuality sets photo quality scores to -1 if files are missing.
|
||||
func ResetPhotoQuality() error {
|
||||
log.Info("index: flagging hidden files")
|
||||
start := time.Now()
|
||||
|
||||
return Db().Table("photos").
|
||||
Where("id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1 AND file_missing = 0 AND deleted_at IS NULL)").
|
||||
Update("photo_quality", -1).Error
|
||||
res := Db().Table("photos").
|
||||
Where("id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1 AND file_missing = 0 AND file_error = '' AND deleted_at IS NULL)").
|
||||
Update("photo_quality", -1)
|
||||
|
||||
if res.RowsAffected == 0 {
|
||||
log.Debugf("index: found no additional broken photos [%s]", time.Since(start))
|
||||
} else {
|
||||
log.Infof("index: flagged %s as hidden [%s]", english.Plural(int(res.RowsAffected), "broken photo", "broken photos"), time.Since(start))
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
// PhotosCheck returns photos selected for maintenance.
|
||||
|
@ -124,7 +134,7 @@ func OrphanPhotos() (photos entity.Photos, err error) {
|
|||
|
||||
// FixPrimaries tries to set a primary file for photos that have none.
|
||||
func FixPrimaries() error {
|
||||
log.Info("index: updating primary files")
|
||||
start := time.Now()
|
||||
|
||||
var photos entity.Photos
|
||||
|
||||
|
@ -136,13 +146,20 @@ func FixPrimaries() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if len(photos) == 0 {
|
||||
log.Debugf("index: found no photos without primary file [%s]", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, p := range photos {
|
||||
log.Debugf("index: finding new primary file for photo %s", p.PhotoUID)
|
||||
log.Debugf("index: searching primary file for %s", p.PhotoUID)
|
||||
|
||||
if err := p.SetPrimary(""); err != nil {
|
||||
log.Infof("index: %s (set primary)", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("index: updated primary files [%s]", time.Since(start))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ func FromFile(imageFilename, hash, thumbPath string, width, height, orientation
|
|||
img, err := imaging.Open(imageFilename)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("resample: %s in %s", err, txt.Quote(filepath.Base(imageFilename)))
|
||||
log.Debugf("resample: %s in %s", err, txt.Quote(filepath.Base(imageFilename)))
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@ func (l *IgnoreList) Dir(dir string) error {
|
|||
}
|
||||
|
||||
if !FileExists(fileName) {
|
||||
return fmt.Errorf("no %s file found", l.configFile)
|
||||
return fmt.Errorf("found no %s file", l.configFile)
|
||||
}
|
||||
|
||||
return l.ConfigFile(fileName)
|
||||
|
|
Loading…
Reference in a new issue