mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Indexer: Improve stacking and indexing of moved files
This commit is contained in:
Binary file not shown.
@@ -1676,7 +1676,7 @@ msgstr "Ähnlich"
|
|||||||
|
|
||||||
#: src/dialog/photo/info.vue:93
|
#: src/dialog/photo/info.vue:93
|
||||||
msgid "Single"
|
msgid "Single"
|
||||||
msgstr "Einzelbild"
|
msgstr "Einzelaufnahme"
|
||||||
|
|
||||||
#: src/dialog/account/edit.vue:183 src/dialog/photo/files.vue:84
|
#: src/dialog/account/edit.vue:183 src/dialog/photo/files.vue:84
|
||||||
#: src/dialog/photo/files.vue:31
|
#: src/dialog/photo/files.vue:31
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -40,7 +40,7 @@ func optimizeAction(ctx *cli.Context) error {
|
|||||||
|
|
||||||
worker := workers.NewMeta(conf)
|
worker := workers.NewMeta(conf)
|
||||||
|
|
||||||
if err := worker.Start(); err != nil {
|
if err := worker.Start(time.Second * 15); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package entity
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -105,7 +106,7 @@ func (m *Photo) Optimize(stackMeta, stackUuid bool) (updated bool, merged Photos
|
|||||||
m.UpdateLocation()
|
m.UpdateLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
if merged, err = m.Stack(stackMeta, stackUuid); err != nil {
|
if merged, err = m.Stack(stackMeta, stackUuid, true); err != nil {
|
||||||
log.Errorf("photo: %s (stack)", err)
|
log.Errorf("photo: %s (stack)", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,51 +146,66 @@ func (m *Photo) Optimize(stackMeta, stackUuid bool) (updated bool, merged Photos
|
|||||||
func (m *Photo) ResolvePrimary() error {
|
func (m *Photo) ResolvePrimary() error {
|
||||||
var file File
|
var file File
|
||||||
|
|
||||||
if err := Db().First(&file, "file_primary = 1 AND photo_id = ?", m.ID).Error; err == nil {
|
if err := Db().Where("file_primary = 1 AND photo_id = ?", m.ID).First(&file).Error; err == nil && file.ID > 0 {
|
||||||
return file.ResolvePrimary()
|
return file.ResolvePrimary()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := Db().First(&file, "file_type = 'jpg' AND photo_id = ?", m.ID).Error; err == nil {
|
return nil
|
||||||
file.FilePrimary = true
|
|
||||||
return file.ResolvePrimary()
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.Update("PhotoQuality", -1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stack merges a photo with identical ones.
|
// Identical returns identical photos that can be merged.
|
||||||
func (m *Photo) Stack(stackMeta, stackUuid bool) (identical Photos, err error) {
|
func (m *Photo) Identical(findMeta, findUuid, findOlder bool) (identical Photos, err error) {
|
||||||
if !stackMeta && !stackUuid || m.PhotoSingle || m.DeletedAt != nil {
|
if !findMeta && !findUuid || m.PhotoSingle || m.DeletedAt != nil {
|
||||||
return identical, nil
|
return identical, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
op := "<>"
|
||||||
|
|
||||||
|
if findOlder {
|
||||||
|
op = "<"
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case stackMeta && stackUuid && m.HasLocation() && m.HasLatLng() && m.TakenSrc == SrcMeta && rnd.IsUUID(m.UUID):
|
case findMeta && findUuid && m.HasLocation() && m.HasLatLng() && m.TakenSrc == SrcMeta && rnd.IsUUID(m.UUID):
|
||||||
if err := Db().Where("id > ? AND photo_single = 0", m.ID).
|
if err := Db().
|
||||||
Where("(taken_at = ? AND taken_src = 'meta' AND cell_id = ? AND camera_serial = ? AND camera_id = ?) OR (uuid <> '' AND uuid = ?)",
|
Where("(taken_at = ? AND taken_src = 'meta' AND cell_id = ? AND camera_serial = ? AND camera_id = ?) OR (uuid <> '' AND uuid = ?)",
|
||||||
m.TakenAt, m.CellID, m.CameraSerial, m.CameraID, m.UUID).Find(&identical).Error; err != nil {
|
m.TakenAt, m.CellID, m.CameraSerial, m.CameraID, m.UUID).
|
||||||
|
Where(fmt.Sprintf("id %s ? AND photo_single = 0 AND deleted_at IS NULL", op), m.ID).
|
||||||
|
Order("id ASC").Find(&identical).Error; err != nil {
|
||||||
return identical, err
|
return identical, err
|
||||||
}
|
}
|
||||||
case stackMeta && m.HasLocation() && m.HasLatLng() && m.TakenSrc == SrcMeta:
|
case findMeta && m.HasLocation() && m.HasLatLng() && m.TakenSrc == SrcMeta:
|
||||||
if err := Db().Where("id > ? AND photo_single = 0", m.ID).
|
if err := Db().
|
||||||
Where("taken_at = ? AND taken_src = 'meta' AND cell_id = ? AND camera_serial = ? AND camera_id = ?",
|
Where("taken_at = ? AND taken_src = 'meta' AND cell_id = ? AND camera_serial = ? AND camera_id = ?",
|
||||||
m.TakenAt, m.CellID, m.CameraSerial, m.CameraID).Error; err != nil {
|
m.TakenAt, m.CellID, m.CameraSerial, m.CameraID).
|
||||||
|
Where(fmt.Sprintf("id %s ? AND photo_single = 0 AND deleted_at IS NULL", op), m.ID).
|
||||||
|
Order("id ASC").Find(&identical).Error; err != nil {
|
||||||
return identical, err
|
return identical, err
|
||||||
}
|
}
|
||||||
case stackUuid && rnd.IsUUID(m.UUID):
|
case findUuid && rnd.IsUUID(m.UUID):
|
||||||
if err := Db().Where("id > ? AND photo_single = 0", m.ID).
|
if err := Db().
|
||||||
Where("uuid <> '' AND uuid = ?", m.UUID).Error; err != nil {
|
Where(fmt.Sprintf("uuid = ? AND id %s ? AND photo_single = 0 AND deleted_at IS NULL", op), m.UUID, m.ID).
|
||||||
|
Order("id ASC").Find(&identical).Error; err != nil {
|
||||||
return identical, err
|
return identical, err
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
return identical, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(identical) == 0 {
|
|
||||||
return identical, nil
|
return identical, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stack merges a photo with identical ones.
|
||||||
|
func (m *Photo) Stack(stackMeta, stackUuid, stackOlder bool) (identical Photos, err error) {
|
||||||
|
identical, err = m.Identical(stackMeta, stackUuid, stackOlder)
|
||||||
|
|
||||||
|
if len(identical) == 0 || err != nil {
|
||||||
|
return identical, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, photo := range identical {
|
for _, photo := range identical {
|
||||||
|
if photo.DeletedAt != nil || photo.ID == m.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if err := UnscopedDb().Exec("UPDATE `files` SET photo_id = ?, photo_uid = ?, file_primary = 0 WHERE photo_id = ?", m.ID, m.PhotoUID, photo.ID).Error; err != nil {
|
if err := UnscopedDb().Exec("UPDATE `files` SET photo_id = ?, photo_uid = ?, file_primary = 0 WHERE photo_id = ?", m.ID, m.PhotoUID, photo.ID).Error; err != nil {
|
||||||
return identical, err
|
return identical, err
|
||||||
}
|
}
|
||||||
@@ -207,9 +223,14 @@ func (m *Photo) Stack(stackMeta, stackUuid bool) (identical Photos, err error) {
|
|||||||
log.Warnf("photo: unknown SQL dialect (stack)")
|
log.Warnf("photo: unknown SQL dialect (stack)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := photo.Updates(map[string]interface{}{"DeletedAt": Timestamp(), "PhotoQuality": -1}); err != nil {
|
deleted := Timestamp()
|
||||||
|
|
||||||
|
if err := UnscopedDb().Exec("UPDATE `photos` SET photo_quality = -1, deleted_at = ? WHERE id = ?", Timestamp(), photo.ID).Error; err != nil {
|
||||||
return identical, err
|
return identical, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
photo.DeletedAt = &deleted
|
||||||
|
photo.PhotoQuality = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
return identical, err
|
return identical, err
|
||||||
|
|||||||
@@ -881,7 +881,7 @@ func TestPhoto_Approve(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, 3, photo.PhotoQuality)
|
assert.Equal(t, 4, photo.PhotoQuality)
|
||||||
})
|
})
|
||||||
t.Run("quality = 1", func(t *testing.T) {
|
t.Run("quality = 1", func(t *testing.T) {
|
||||||
photo := Photo{PhotoQuality: 1}
|
photo := Photo{PhotoQuality: 1}
|
||||||
|
|||||||
@@ -175,10 +175,10 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||||||
// Look for existing photo if file wasn't indexed yet...
|
// Look for existing photo if file wasn't indexed yet...
|
||||||
if !fileExists {
|
if !fileExists {
|
||||||
fullBase := m.BasePrefix(false)
|
fullBase := m.BasePrefix(false)
|
||||||
photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name IN (?)", filePath, []string{fullBase, fileBase})
|
|
||||||
|
|
||||||
if photoQuery.Error == nil {
|
if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fileBase); photoQuery.Error == nil || fileBase == fullBase {
|
||||||
fileBase = photo.PhotoName
|
// Skip next query.
|
||||||
|
} else if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fullBase); photoQuery.Error == nil {
|
||||||
fileStacked = true
|
fileStacked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||||||
// Flag first JPEG as primary file for this photo.
|
// Flag first JPEG as primary file for this photo.
|
||||||
if !file.FilePrimary {
|
if !file.FilePrimary {
|
||||||
if photoExists {
|
if photoExists {
|
||||||
if q := entity.Db().Where("file_type = 'jpg' AND file_primary = 1 AND photo_id = ?", photo.ID).First(&primaryFile); q.Error != nil {
|
if q := entity.UnscopedDb().Where("file_type = 'jpg' AND file_primary = 1 AND photo_id = ?", photo.ID).First(&primaryFile); q.Error != nil {
|
||||||
file.FilePrimary = m.IsJpeg()
|
file.FilePrimary = m.IsJpeg()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -815,8 +815,13 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||||||
log.Errorf("index: %s in %s (set download id)", err, logName)
|
log.Errorf("index: %s in %s (set download id)", err, logName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if stacked, err := photo.Stack(Config().Settings().StackMeta(), Config().Settings().StackUUID(), true); err != nil {
|
||||||
|
log.Errorf("index: %s in %s (stack)", err.Error(), logName)
|
||||||
|
} else if len(stacked) > 0 {
|
||||||
|
log.Infof("index: merged %s with existing stack", logName)
|
||||||
|
result.Status = IndexStacked
|
||||||
|
} else if file.FilePrimary && Config().SidecarYaml() {
|
||||||
// Write YAML sidecar file (optional).
|
// Write YAML sidecar file (optional).
|
||||||
if file.FilePrimary && Config().SidecarYaml() {
|
|
||||||
yamlFile := photo.YamlFileName(Config().OriginalsPath(), Config().SidecarPath())
|
yamlFile := photo.YamlFileName(Config().OriginalsPath(), Config().SidecarPath())
|
||||||
|
|
||||||
if err := photo.SaveAsYaml(yamlFile); err != nil {
|
if err := photo.SaveAsYaml(yamlFile); err != nil {
|
||||||
|
|||||||
@@ -1006,10 +1006,12 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) {
|
|||||||
func (m *MediaFile) RenameSidecars(oldFileName string) (renamed map[string]string, err error) {
|
func (m *MediaFile) RenameSidecars(oldFileName string) (renamed map[string]string, err error) {
|
||||||
renamed = make(map[string]string)
|
renamed = make(map[string]string)
|
||||||
|
|
||||||
newName := m.RelPrefix(Config().OriginalsPath(), false)
|
sidecarPath := Config().SidecarPath()
|
||||||
|
originalsPath := Config().OriginalsPath()
|
||||||
|
|
||||||
oldPrefix := fs.RelPrefix(oldFileName, Config().OriginalsPath(), false)
|
newName := m.RelPrefix(originalsPath, false)
|
||||||
globPrefix := filepath.Join(Config().SidecarPath(), oldPrefix) + "."
|
oldPrefix := fs.RelPrefix(oldFileName, originalsPath, false)
|
||||||
|
globPrefix := filepath.Join(sidecarPath, oldPrefix) + "."
|
||||||
|
|
||||||
matches, err := filepath.Glob(regexp.QuoteMeta(globPrefix) + "*")
|
matches, err := filepath.Glob(regexp.QuoteMeta(globPrefix) + "*")
|
||||||
|
|
||||||
@@ -1018,12 +1020,25 @@ func (m *MediaFile) RenameSidecars(oldFileName string) (renamed map[string]strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, srcName := range matches {
|
for _, srcName := range matches {
|
||||||
destName := filepath.Join(Config().SidecarPath(), newName+filepath.Ext(srcName))
|
destName := filepath.Join(sidecarPath, newName+filepath.Ext(srcName))
|
||||||
|
|
||||||
|
if fs.FileExists(destName) {
|
||||||
|
renamed[fs.RelName(srcName, sidecarPath)] = fs.RelName(destName, sidecarPath)
|
||||||
|
|
||||||
|
if err := os.Remove(srcName); err != nil {
|
||||||
|
log.Errorf("media: failed removing sidecar %s", txt.Quote(fs.RelName(srcName, sidecarPath)))
|
||||||
|
} else {
|
||||||
|
log.Infof("media: removed sidecar %s", txt.Quote(fs.RelName(srcName, sidecarPath)))
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if err := fs.Move(srcName, destName); err != nil {
|
if err := fs.Move(srcName, destName); err != nil {
|
||||||
return renamed, err
|
return renamed, err
|
||||||
} else {
|
} else {
|
||||||
renamed[fs.RelName(srcName, Config().SidecarPath())] = fs.RelName(destName, Config().SidecarPath())
|
log.Infof("media: moved existing sidecar to %s", txt.Quote(newName+filepath.Ext(srcName)))
|
||||||
|
renamed[fs.RelName(srcName, sidecarPath)] = fs.RelName(destName, sidecarPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func RenameFile(srcRoot, srcName, destRoot, destName string) error {
|
|||||||
return fmt.Errorf("can't rename %s/%s to %s/%s", srcRoot, srcName, destRoot, destName)
|
return fmt.Errorf("can't rename %s/%s to %s/%s", srcRoot, srcName, destRoot, destName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Db().Exec("UPDATE files SET file_root = ?, file_name = ?, file_missing = ?, deleted_at = NULL WHERE file_root = ? AND file_name = ?", destRoot, destName, srcRoot, srcName).Error
|
return Db().Exec("UPDATE files SET file_root = ?, file_name = ?, file_missing = 0, deleted_at = NULL WHERE file_root = ? AND file_name = ?", destRoot, destName, srcRoot, srcName).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPhotoPrimary sets a new primary image file for a photo.
|
// SetPhotoPrimary sets a new primary image file for a photo.
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func ResetPhotoQuality() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PhotosCheck returns photos selected for maintenance.
|
// PhotosCheck returns photos selected for maintenance.
|
||||||
func PhotosCheck(limit int, offset int) (entities entity.Photos, err error) {
|
func PhotosCheck(limit, offset int, delay time.Duration) (entities entity.Photos, err error) {
|
||||||
err = Db().
|
err = Db().
|
||||||
Preload("Labels", func(db *gorm.DB) *gorm.DB {
|
Preload("Labels", func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
|
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
|
||||||
@@ -112,7 +112,7 @@ func PhotosCheck(limit int, offset int) (entities entity.Photos, err error) {
|
|||||||
Preload("Cell").
|
Preload("Cell").
|
||||||
Preload("Cell.Place").
|
Preload("Cell.Place").
|
||||||
Where("checked_at IS NULL OR checked_at < ?", time.Now().Add(-1*time.Hour*24*3)).
|
Where("checked_at IS NULL OR checked_at < ?", time.Now().Add(-1*time.Hour*24*3)).
|
||||||
Where("updated_at < ? OR (cell_id = 'zz' AND photo_lat <> 0)", time.Now().Add(-1*time.Minute*10)).
|
Where("updated_at < ? OR (cell_id = 'zz' AND photo_lat <> 0)", time.Now().Add(-1*delay)).
|
||||||
Order("photos.ID ASC").Limit(limit).Offset(offset).Find(&entities).Error
|
Order("photos.ID ASC").Limit(limit).Offset(offset).Find(&entities).Error
|
||||||
|
|
||||||
return entities, err
|
return entities, err
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package query
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ func TestResetPhotosQuality(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPhotosCheck(t *testing.T) {
|
func TestPhotosCheck(t *testing.T) {
|
||||||
result, err := PhotosCheck(10, 0)
|
result, err := PhotosCheck(10, 0, time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (worker *Meta) originalsPath() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the metadata worker.
|
// Start starts the metadata worker.
|
||||||
func (worker *Meta) Start() (err error) {
|
func (worker *Meta) Start(delay time.Duration) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = fmt.Errorf("metadata: %s (panic)\nstack: %s", r, debug.Stack())
|
err = fmt.Errorf("metadata: %s (panic)\nstack: %s", r, debug.Stack())
|
||||||
@@ -53,7 +53,7 @@ func (worker *Meta) Start() (err error) {
|
|||||||
optimized := 0
|
optimized := 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
photos, err := query.PhotosCheck(limit, offset)
|
photos, err := query.PhotosCheck(limit, offset, delay)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package workers
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/mutex"
|
"github.com/photoprism/photoprism/internal/mutex"
|
||||||
@@ -22,13 +23,13 @@ func TestPrism_Start(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := worker.Start(); err == nil {
|
if err := worker.Start(time.Second); err == nil {
|
||||||
t.Fatal("error expected")
|
t.Fatal("error expected")
|
||||||
}
|
}
|
||||||
|
|
||||||
mutex.MetaWorker.Stop()
|
mutex.MetaWorker.Stop()
|
||||||
|
|
||||||
if err := worker.Start(); err != nil {
|
if err := worker.Start(time.Second); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func StartMeta(conf *config.Config) {
|
|||||||
if !mutex.WorkersBusy() {
|
if !mutex.WorkersBusy() {
|
||||||
go func() {
|
go func() {
|
||||||
worker := NewMeta(conf)
|
worker := NewMeta(conf)
|
||||||
if err := worker.Start(); err != nil {
|
if err := worker.Start(time.Minute); err != nil {
|
||||||
log.Warnf("metadata: %s", err)
|
log.Warnf("metadata: %s", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
Reference in New Issue
Block a user