People: Add subject cache and show real name in logs #1438 #2182

This commit is contained in:
Michael Mayer
2022-04-04 00:02:08 +02:00
parent 41b252d820
commit 686f6bc47c
32 changed files with 396 additions and 102 deletions

View File

@@ -63,7 +63,7 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
log.Errorf("archive: %s", err) log.Errorf("archive: %s", err)
AbortSaveFailed(c) AbortSaveFailed(c)
return return
} else if err := entity.Db().Model(&entity.PhotoAlbum{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("hidden", true).Error; err != nil { } else if err := entity.Db().Model(&entity.PhotoAlbum{}).Where("photo_uid IN (?)", f.Photos).Update("hidden", true).Error; err != nil {
log.Errorf("archive: %s", err) log.Errorf("archive: %s", err)
} }
@@ -124,7 +124,7 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
} }
} }
} else if err := entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uid IN (?)", f.Photos). } else if err := entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uid IN (?)", f.Photos).
UpdateColumn("deleted_at", gorm.Expr("NULL")).Error; err != nil { Update("deleted_at", gorm.Expr("NULL")).Error; err != nil {
log.Errorf("restore: %s", err) log.Errorf("restore: %s", err)
AbortSaveFailed(c) AbortSaveFailed(c)
return return
@@ -260,7 +260,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
log.Infof("photos: updating private flag for %s", sanitize.Log(f.String())) log.Infof("photos: updating private flag for %s", sanitize.Log(f.String()))
if err := entity.Db().Model(entity.Photo{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("photo_private", if err := entity.Db().Model(entity.Photo{}).Where("photo_uid IN (?)", f.Photos).Update("photo_private",
gorm.Expr("CASE WHEN photo_private > 0 THEN 0 ELSE 1 END")).Error; err != nil { gorm.Expr("CASE WHEN photo_private > 0 THEN 0 ELSE 1 END")).Error; err != nil {
log.Errorf("private: %s", err) log.Errorf("private: %s", err)
AbortSaveFailed(c) AbortSaveFailed(c)

View File

@@ -211,12 +211,12 @@ func (m *Account) Directories() (result fs.FileInfos, err error) {
// Updates multiple columns in the database. // Updates multiple columns in the database.
func (m *Account) Updates(values interface{}) error { func (m *Account) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error return UnscopedDb().Model(m).Updates(values).Error
} }
// Update a column in the database. // Update a column in the database.
func (m *Account) Update(attr string, value interface{}) error { func (m *Account) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error return UnscopedDb().Model(m).Update(attr, value).Error
} }
// Save updates the existing or inserts a new row. // Save updates the existing or inserts a new row.

View File

@@ -483,12 +483,12 @@ func (m *Album) SaveForm(f form.Album) error {
// Update sets a new value for a database column. // Update sets a new value for a database column.
func (m *Album) Update(attr string, value interface{}) error { func (m *Album) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error return UnscopedDb().Model(m).Update(attr, value).Error
} }
// Updates multiple columns in the database. // Updates multiple columns in the database.
func (m *Album) Updates(values interface{}) error { func (m *Album) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error return UnscopedDb().Model(m).Updates(values).Error
} }
// UpdateFolder updates the path, filter and slug for a folder album. // UpdateFolder updates the path, filter and slug for a folder album.
@@ -500,7 +500,7 @@ func (m *Album) UpdateFolder(albumPath, albumFilter string) error {
return nil return nil
} }
if err := UnscopedDb().Model(m).UpdateColumns(map[string]interface{}{ if err := UnscopedDb().Model(m).Updates(map[string]interface{}{
"AlbumPath": albumPath, "AlbumPath": albumPath,
"AlbumFilter": albumFilter, "AlbumFilter": albumFilter,
"AlbumSlug": albumSlug, "AlbumSlug": albumSlug,

160
internal/entity/cache.go Normal file
View File

@@ -0,0 +1,160 @@
package entity
import (
"strings"
"sync"
)
type CacheItem interface {
CacheUID() string
CacheName() string
}
type CacheItems []CacheItem
type Cached map[string]CacheItem
// Names maps names with unique ids.
func (c Cached) Names() map[string]string {
names := make(map[string]string, len(c))
for uid := range c {
item := c[uid].(CacheItem)
if name := item.CacheName(); name != "" {
names[strings.ToLower(name)] = item.CacheUID()
}
}
return names
}
// Cache represents a lightweight entity cache.
type Cache struct {
count int
items Cached
names map[string]string
mutex sync.RWMutex
}
// NewCache creates a new NewCache instance.
func NewCache(items Cached) *Cache {
m := &Cache{
items: items,
count: len(items),
names: items.Names(),
mutex: sync.RWMutex{},
}
return m
}
// Count returns the number of cached items.
func (c *Cache) Count() int {
return c.count
}
// Set adds or updates an item.
func (c *Cache) Set(item CacheItem) {
c.mutex.Lock()
defer c.mutex.Unlock()
uid := item.CacheUID()
name := strings.ToLower(item.CacheName())
// Update name index.
if o, found := c.items[uid]; !found {
if name != "" {
c.names[name] = uid
}
} else if n := strings.ToLower(o.(CacheItem).CacheName()); n != name {
delete(c.names, n)
}
// Set item.
c.items[uid] = item
// Update count.
c.count = len(c.items)
}
// UID finds and returns a cached item by unique id.
func (c *Cache) UID(uid string) (item CacheItem, found bool) {
if c.count == 0 || uid == "" {
return nil, false
}
c.mutex.RLock()
defer c.mutex.RUnlock()
if result, ok := c.items[uid]; ok && result != nil {
return result, true
} else {
return nil, false
}
}
// Name finds and returns a cached item by common name.
func (c *Cache) Name(name string) (item CacheItem, found bool) {
if c.count == 0 || name == "" {
return nil, false
}
c.mutex.RLock()
defer c.mutex.RUnlock()
name = strings.ToLower(name)
if uid, hasUid := c.names[name]; !hasUid {
return nil, false
} else if result, ok := c.items[uid]; ok && result != nil {
return result, true
} else {
return nil, false
}
}
// Remove a file from the lookup table.
func (c *Cache) Remove(uid string) {
if c.count == 0 {
return
}
c.mutex.Lock()
defer c.mutex.Unlock()
// Delete from name index.
if item, found := c.items[uid]; !found || item == nil {
return
} else if name := strings.ToLower(item.(CacheItem).CacheName()); name != "" {
delete(c.names, name)
}
// Delete from items.
delete(c.items, uid)
// Update count.
c.count = len(c.items)
}
// Exists tests of a file exists.
func (c *Cache) Exists(uid string) bool {
if c.count == 0 {
return false
}
c.mutex.RLock()
defer c.mutex.RUnlock()
if _, ok := c.items[uid]; ok {
return true
} else {
return false
}
}
// NameExists checks if the item name is known.
func (c *Cache) NameExists(name string) bool {
_, found := c.Name(name)
return found
}

View File

@@ -123,7 +123,7 @@ func (m *Cell) Refresh(api string) (err error) {
log.Tracef("index: cell %s keeps place_id %s", m.ID, m.PlaceID) log.Tracef("index: cell %s keeps place_id %s", m.ID, m.PlaceID)
} else if err := UnscopedDb().Table(Photo{}.TableName()). } else if err := UnscopedDb().Table(Photo{}.TableName()).
Where("place_id = ?", oldPlaceID). Where("place_id = ?", oldPlaceID).
UpdateColumn("place_id", m.PlaceID). Update("place_id", m.PlaceID).
Error; err != nil { Error; err != nil {
log.Warnf("index: %s while changing place_id from %s to %s", err, oldPlaceID, m.PlaceID) log.Warnf("index: %s while changing place_id from %s to %s", err, oldPlaceID, m.PlaceID)
} }

View File

@@ -56,7 +56,7 @@ func UpdatePlacesCounts() (err error) {
// Update places. // Update places.
res := Db().Table("places"). res := Db().Table("places").
UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(*) FROM photos p "+ Update("photo_count", gorm.Expr("(SELECT COUNT(*) FROM photos p "+
"WHERE places.id = p.place_id "+ "WHERE places.id = p.place_id "+
"AND p.photo_quality >= 0 "+ "AND p.photo_quality >= 0 "+
"AND p.photo_private = 0 "+ "AND p.photo_private = 0 "+
@@ -99,7 +99,7 @@ func UpdateSubjectCounts() (err error) {
case SQLite3: case SQLite3:
// Update files count. // Update files count.
res = Db().Table(subjTable). res = Db().Table(subjTable).
UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(DISTINCT f.id) FROM files f "+ Update("file_count", gorm.Expr("(SELECT COUNT(DISTINCT f.id) FROM files f "+
fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ", fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ",
markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition)) markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition))
@@ -108,7 +108,7 @@ func UpdateSubjectCounts() (err error) {
return res.Error return res.Error
} else { } else {
photosRes := Db().Table(subjTable). photosRes := Db().Table(subjTable).
UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(DISTINCT f.photo_id) FROM files f "+ Update("photo_count", gorm.Expr("(SELECT COUNT(DISTINCT f.photo_id) FROM files f "+
fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ", fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ",
markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition)) markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition))
res.RowsAffected += photosRes.RowsAffected res.RowsAffected += photosRes.RowsAffected
@@ -150,7 +150,7 @@ func UpdateLabelCounts() (err error) {
} else if IsDialect(SQLite3) { } else if IsDialect(SQLite3) {
res = Db(). res = Db().
Table("labels"). Table("labels").
UpdateColumn("photo_count", Update("photo_count",
gorm.Expr(`(SELECT photo_count FROM (SELECT label_id, SUM(photo_count) AS photo_count FROM ( gorm.Expr(`(SELECT photo_count FROM (SELECT label_id, SUM(photo_count) AS photo_count FROM (
SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
JOIN photos_labels pl ON pl.label_id = l.id JOIN photos_labels pl ON pl.label_id = l.id

View File

@@ -91,7 +91,7 @@ func (m *Face) SetEmbeddings(embeddings face.Embeddings) (err error) {
// Matched updates the match timestamp. // Matched updates the match timestamp.
func (m *Face) Matched() error { func (m *Face) Matched() error {
m.MatchedAt = TimePointer() m.MatchedAt = TimePointer()
return UnscopedDb().Model(m).UpdateColumns(Values{"MatchedAt": m.MatchedAt}).Error return UnscopedDb().Model(m).Updates(Values{"MatchedAt": m.MatchedAt}).Error
} }
// Embedding returns parsed face embedding. // Embedding returns parsed face embedding.
@@ -166,7 +166,7 @@ func (m *Face) ResolveCollision(embeddings face.Embeddings) (resolved bool, err
return false, fmt.Errorf("collision distance must be positive") return false, fmt.Errorf("collision distance must be positive")
} else if dist < 0.02 { } else if dist < 0.02 {
// Ignore if distance is very small as faces may belong to the same person. // Ignore if distance is very small as faces may belong to the same person.
log.Warnf("face %s: clearing ambiguous subject %s, similar face at dist %f with source %s", m.ID, m.SubjUID, dist, SrcString(m.FaceSrc)) log.Warnf("face %s: clearing ambiguous subject %s, similar face at dist %f with source %s", m.ID, LogSubj(m.SubjUID), dist, SrcString(m.FaceSrc))
// Reset subject UID just in case. // Reset subject UID just in case.
m.SubjUID = "" m.SubjUID = ""
@@ -260,7 +260,7 @@ func (m *Face) SetSubjectUID(subjUID string) (err error) {
Where("subj_src = ?", SrcAuto). Where("subj_src = ?", SrcAuto).
Where("subj_uid <> ?", m.SubjUID). Where("subj_uid <> ?", m.SubjUID).
Where("marker_invalid = 0"). Where("marker_invalid = 0").
UpdateColumns(Values{"subj_uid": m.SubjUID, "marker_review": false}).Error; err != nil { Updates(Values{"subj_uid": m.SubjUID, "marker_review": false}).Error; err != nil {
return err return err
} }
@@ -319,7 +319,7 @@ func (m *Face) Delete() error {
// Remove face id from markers before deleting. // Remove face id from markers before deleting.
if err := Db().Model(&Marker{}). if err := Db().Model(&Marker{}).
Where("face_id = ?", m.ID). Where("face_id = ?", m.ID).
UpdateColumns(Values{"face_id": "", "face_dist": -1}).Error; err != nil { Updates(Values{"face_id": "", "face_dist": -1}).Error; err != nil {
return err return err
} }
@@ -341,12 +341,14 @@ func FirstOrCreateFace(m *Face) *Face {
result := Face{} result := Face{}
if err := UnscopedDb().Where("id = ?", m.ID).First(&result).Error; err == nil { if err := UnscopedDb().Where("id = ?", m.ID).First(&result).Error; err == nil {
log.Warnf("faces: %s has ambiguous subject %s", m.ID, m.SubjUID) if m.SubjUID != result.SubjUID {
log.Warnf("faces: %s has ambiguous subject %s", m.ID, LogSubj(m.SubjUID))
}
return &result return &result
} else if createErr := m.Create(); createErr == nil { } else if createErr := m.Create(); createErr == nil {
return m return m
} else if err := UnscopedDb().Where("id = ?", m.ID).First(&result).Error; err == nil { } else if err := UnscopedDb().Where("id = ?", m.ID).First(&result).Error; err == nil {
log.Warnf("faces: %s has ambiguous subject %s", m.ID, m.SubjUID) log.Warnf("faces: %s has ambiguous subject %s", m.ID, LogSubj(m.SubjUID))
return &result return &result
} else { } else {
log.Errorf("faces: %s when trying to create %s", createErr, m.ID) log.Errorf("faces: %s when trying to create %s", createErr, m.ID)

View File

@@ -222,11 +222,17 @@ func TestFace_RefreshPhotos(t *testing.T) {
} }
func TestFirstOrCreateFace(t *testing.T) { func TestFirstOrCreateFace(t *testing.T) {
t.Run("create new face", func(t *testing.T) { t.Run("CreateNew", func(t *testing.T) {
m := NewFace("12345unique", SrcAuto, face.Embeddings{face.Embedding{99}, face.Embedding{2}}) m := NewFace("12345unique", SrcAuto, face.Embeddings{face.Embedding{99}, face.Embedding{2}})
r := FirstOrCreateFace(m) r := FirstOrCreateFace(m)
assert.Equal(t, "12345unique", r.SubjUID) assert.Equal(t, "12345unique", r.SubjUID)
}) })
t.Run("FindExisting", func(t *testing.T) {
m := FaceFixtures.Pointer("joe-biden")
r := FirstOrCreateFace(m)
assert.Equal(t, "jqy3y652h8njw0sx", r.SubjUID)
assert.Equal(t, 33, r.Samples)
})
t.Run("return existing entity", func(t *testing.T) { t.Run("return existing entity", func(t *testing.T) {
m := FaceFixtures.Pointer("joe-biden") m := FaceFixtures.Pointer("joe-biden")
r := FirstOrCreateFace(m) r := FirstOrCreateFace(m)

View File

@@ -331,7 +331,7 @@ func (m *File) ReplaceHash(newHash string) error {
for name, entity := range entities { for name, entity := range entities {
start := time.Now() start := time.Now()
if res := UnscopedDb().Model(entity).Where("thumb = ?", oldHash).UpdateColumn("thumb", newHash); res.Error != nil { if res := UnscopedDb().Model(entity).Where("thumb = ?", oldHash).Update("thumb", newHash); res.Error != nil {
return res.Error return res.Error
} else if res.RowsAffected > 0 { } else if res.RowsAffected > 0 {
log.Infof("%s: updated %s [%s]", name, english.Plural(int(res.RowsAffected), "cover", "covers"), time.Since(start)) log.Infof("%s: updated %s [%s]", name, english.Plural(int(res.RowsAffected), "cover", "covers"), time.Since(start))
@@ -453,12 +453,12 @@ func (m *File) UpdateVideoInfos() error {
// Update updates a column in the database. // Update updates a column in the database.
func (m *File) Update(attr string, value interface{}) error { func (m *File) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error return UnscopedDb().Model(m).Update(attr, value).Error
} }
// Updates multiple columns in the database. // Updates multiple columns in the database.
func (m *File) Updates(values interface{}) error { func (m *File) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error return UnscopedDb().Model(m).Updates(values).Error
} }
// Rename updates the name and path of this file. // Rename updates the name and path of this file.
@@ -644,7 +644,7 @@ func (m *File) UpdatePhotoFaceCount() (c int, err error) {
err = UnscopedDb().Model(Photo{}). err = UnscopedDb().Model(Photo{}).
Where("id = ?", m.PhotoID). Where("id = ?", m.PhotoID).
UpdateColumn("photo_faces", c).Error Update("photo_faces", c).Error
return c, err return c, err
} }

View File

@@ -46,12 +46,12 @@ func NewFileShare(fileID, accountID uint, remoteName string) *FileShare {
// Updates multiple columns in the database. // Updates multiple columns in the database.
func (m *FileShare) Updates(values interface{}) error { func (m *FileShare) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error return UnscopedDb().Model(m).Updates(values).Error
} }
// Updates a column in the database. // Updates a column in the database.
func (m *FileShare) Update(attr string, value interface{}) error { func (m *FileShare) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error return UnscopedDb().Model(m).Update(attr, value).Error
} }
// Save updates the existing or inserts a new row. // Save updates the existing or inserts a new row.

View File

@@ -46,12 +46,12 @@ func NewFileSync(accountID uint, remoteName string) *FileSync {
// Updates multiple columns in the database. // Updates multiple columns in the database.
func (m *FileSync) Updates(values interface{}) error { func (m *FileSync) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error return UnscopedDb().Model(m).Updates(values).Error
} }
// Update a column in the database. // Update a column in the database.
func (m *FileSync) Update(attr string, value interface{}) error { func (m *FileSync) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error return UnscopedDb().Model(m).Update(attr, value).Error
} }
// Save updates the existing or inserts a new row. // Save updates the existing or inserts a new row.

View File

@@ -571,7 +571,18 @@ func TestFile_SubjectNames(t *testing.T) {
t.Run("Video.jpg", func(t *testing.T) { t.Run("Video.jpg", func(t *testing.T) {
m := FileFixtures.Get("Video.jpg") m := FileFixtures.Get("Video.jpg")
for _, marker := range *m.Markers() {
t.Logf("MarkerUID: %s SubjUID: %s FileUID: %s", marker.MarkerUID, marker.SubjUID, marker.FileUID)
}
for uid, i := range (Subject{}).Cache().items {
t.Logf("UID: %s CacheUID: %s CacheName: %s", uid, i.(CacheItem).CacheUID(), i.(CacheItem).CacheName())
}
for name, uid := range (Subject{}).Cache().names {
t.Logf("Name: %s UID: %s", name, uid)
}
names := m.SubjectNames() names := m.SubjectNames()
t.Log(len(names)) t.Log(len(names))
if len(names) != 1 { if len(names) != 1 {
t.Errorf("there should be one name: %#v", names) t.Errorf("there should be one name: %#v", names)

View File

@@ -29,12 +29,12 @@ func NewKeyword(keyword string) *Keyword {
// Updates multiple columns in the database. // Updates multiple columns in the database.
func (m *Keyword) Updates(values interface{}) error { func (m *Keyword) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error return UnscopedDb().Model(m).Updates(values).Error
} }
// Update a column in the database. // Update a column in the database.
func (m *Keyword) Update(attr string, value interface{}) error { func (m *Keyword) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error return UnscopedDb().Model(m).Update(attr, value).Error
} }
// Save updates the existing or inserts a new row. // Save updates the existing or inserts a new row.

View File

@@ -115,7 +115,7 @@ func (m *Label) Restore() error {
// Update a label property in the database. // Update a label property in the database.
func (m *Label) Update(attr string, value interface{}) error { func (m *Label) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error return UnscopedDb().Model(m).Update(attr, value).Error
} }
// FirstOrCreateLabel returns the existing label, inserts a new label or nil in case of errors. // FirstOrCreateLabel returns the existing label, inserts a new label or nil in case of errors.

View File

@@ -58,7 +58,7 @@ func NewLink(shareUID string, canComment, canEdit bool) Link {
func (m *Link) Redeem() { func (m *Link) Redeem() {
m.LinkViews += 1 m.LinkViews += 1
result := Db().Model(m).UpdateColumn("LinkViews", m.LinkViews) result := Db().Model(m).Update("LinkViews", m.LinkViews)
if result.RowsAffected == 0 { if result.RowsAffected == 0 {
log.Warnf("link: failed updating share view counter for %s", m.LinkUID) log.Warnf("link: failed updating share view counter for %s", m.LinkUID)

View File

@@ -133,7 +133,7 @@ func (m *Marker) UpdateFile(file *File) (updated bool) {
if !updated || m.MarkerUID == "" { if !updated || m.MarkerUID == "" {
return false return false
} else if res := UnscopedDb().Model(m).UpdateColumns(Values{"file_uid": m.FileUID, "thumb": m.Thumb}); res.Error != nil { } else if res := UnscopedDb().Model(m).Updates(Values{"file_uid": m.FileUID, "thumb": m.Thumb}); res.Error != nil {
log.Errorf("marker %s: %s (set file)", m.MarkerUID, res.Error) log.Errorf("marker %s: %s (set file)", m.MarkerUID, res.Error)
return false return false
} else { } else {
@@ -342,7 +342,7 @@ func (m *Marker) SyncSubject(updateRelated bool) (err error) {
// Update related markers? // Update related markers?
if m.FaceID == "" || m.SubjUID == "" { if m.FaceID == "" || m.SubjUID == "" {
// Do nothing. // Do nothing.
} else if res := Db().Model(&Face{}).Where("id = ? AND subj_uid = ''", m.FaceID).UpdateColumn("subj_uid", m.SubjUID); res.Error != nil { } else if res := Db().Model(&Face{}).Where("id = ? AND subj_uid = ''", m.FaceID).Update("subj_uid", m.SubjUID); res.Error != nil {
return fmt.Errorf("%s (update known face)", err) return fmt.Errorf("%s (update known face)", err)
} else if !updateRelated { } else if !updateRelated {
return nil return nil
@@ -351,7 +351,7 @@ func (m *Marker) SyncSubject(updateRelated bool) (err error) {
Where("face_id = ?", m.FaceID). Where("face_id = ?", m.FaceID).
Where("subj_src = ?", SrcAuto). Where("subj_src = ?", SrcAuto).
Where("subj_uid <> ?", m.SubjUID). Where("subj_uid <> ?", m.SubjUID).
UpdateColumns(Values{"subj_uid": m.SubjUID, "subj_src": SrcAuto, "marker_review": false}).Error; err != nil { Updates(Values{"subj_uid": m.SubjUID, "subj_src": SrcAuto, "marker_review": false}).Error; err != nil {
return fmt.Errorf("%s (update related markers)", err) return fmt.Errorf("%s (update related markers)", err)
} else if res.RowsAffected > 0 && m.face != nil { } else if res.RowsAffected > 0 && m.face != nil {
log.Debugf("markers: matched %s with %s", subj.SubjName, m.FaceID) log.Debugf("markers: matched %s with %s", subj.SubjName, m.FaceID)
@@ -574,7 +574,7 @@ func (m *Marker) RefreshPhotos() error {
// Matched updates the match timestamp. // Matched updates the match timestamp.
func (m *Marker) Matched() error { func (m *Marker) Matched() error {
m.MatchedAt = TimePointer() m.MatchedAt = TimePointer()
return UnscopedDb().Model(m).UpdateColumns(Values{"MatchedAt": m.MatchedAt}).Error return UnscopedDb().Model(m).Updates(Values{"MatchedAt": m.MatchedAt}).Error
} }
// Top returns the top Y coordinate as float64. // Top returns the top Y coordinate as float64.

View File

@@ -697,7 +697,7 @@ func (m *Photo) AllFiles() (files Files) {
func (m *Photo) Archive() error { func (m *Photo) Archive() error {
deletedAt := TimeStamp() deletedAt := TimeStamp()
if err := Db().Model(&PhotoAlbum{}).Where("photo_uid = ?", m.PhotoUID).UpdateColumn("hidden", true).Error; err != nil { if err := Db().Model(&PhotoAlbum{}).Where("photo_uid = ?", m.PhotoUID).Update("hidden", true).Error; err != nil {
return err return err
} else if err := m.Update("deleted_at", deletedAt); err != nil { } else if err := m.Update("deleted_at", deletedAt); err != nil {
return err return err
@@ -780,12 +780,12 @@ func (m *Photo) NoDescription() bool {
// Update a column in the database. // Update a column in the database.
func (m *Photo) Update(attr string, value interface{}) error { func (m *Photo) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error return UnscopedDb().Model(m).Update(attr, value).Error
} }
// Updates multiple columns in the database. // Updates multiple columns in the database.
func (m *Photo) Updates(values interface{}) error { func (m *Photo) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error return UnscopedDb().Model(m).Updates(values).Error
} }
// SetFavorite updates the favorite flag of a photo. // SetFavorite updates the favorite flag of a photo.
@@ -886,10 +886,10 @@ func (m *Photo) SetPrimary(fileUID string) (err error) {
if err = Db().Model(File{}). if err = Db().Model(File{}).
Where("photo_uid = ? AND file_uid <> ?", m.PhotoUID, fileUID). Where("photo_uid = ? AND file_uid <> ?", m.PhotoUID, fileUID).
UpdateColumn("file_primary", 0).Error; err != nil { Update("file_primary", 0).Error; err != nil {
return err return err
} else if err = Db().Model(File{}).Where("photo_uid = ? AND file_uid = ?", m.PhotoUID, fileUID). } else if err = Db().Model(File{}).Where("photo_uid = ? AND file_uid = ?", m.PhotoUID, fileUID).
UpdateColumn("file_primary", 1).Error; err != nil { Update("file_primary", 1).Error; err != nil {
return err return err
} else if m.PhotoQuality < 0 { } else if m.PhotoQuality < 0 {
m.PhotoQuality = 0 m.PhotoQuality = 0

View File

@@ -36,12 +36,12 @@ func NewPhotoLabel(photoID, labelID uint, uncertainty int, source string) *Photo
// Updates multiple columns in the database. // Updates multiple columns in the database.
func (m *PhotoLabel) Updates(values interface{}) error { func (m *PhotoLabel) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error return UnscopedDb().Model(m).Updates(values).Error
} }
// Update a column in the database. // Update a column in the database.
func (m *PhotoLabel) Update(attr string, value interface{}) error { func (m *PhotoLabel) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error return UnscopedDb().Model(m).Update(attr, value).Error
} }
// Save saves the entity in the database. // Save saves the entity in the database.

View File

@@ -98,7 +98,7 @@ func (m *Subject) Create() error {
} }
// Delete marks the entity as deleted in the database. // Delete marks the entity as deleted in the database.
func (m *Subject) Delete() error { func (m *Subject) Delete() (err error) {
if m.Deleted() { if m.Deleted() {
return nil return nil
} }
@@ -117,20 +117,36 @@ func (m *Subject) Delete() error {
}) })
} }
if err := Db().Model(&Face{}).Where("subj_uid = ?", m.SubjUID).Update("subj_uid", "").Error; err != nil { if err = Db().Model(&Face{}).Where("subj_uid = ?", m.SubjUID).Update("subj_uid", "").Error; err != nil {
return err return err
} }
return Db().Delete(m).Error err = Db().Delete(m).Error
return err
} }
// AfterDelete resets file and photo counters when the entity was deleted. // AfterDelete resets file and photo counters when the entity was deleted.
func (m *Subject) AfterDelete(tx *gorm.DB) (err error) { func (m *Subject) AfterDelete(db *gorm.DB) (err error) {
tx.Model(m).Updates(Values{ err = db.Model(m).Updates(Values{
"FileCount": 0, "FileCount": 0,
"PhotoCount": 0, "PhotoCount": 0,
}) }).Error
return m.UpdateCache()
return err
}
// AfterSave is a hooks called after creation and updating.
func (m *Subject) AfterSave() error {
log.Debugf("AfterSave: %s %s %t", m.SubjUID, m.SubjName, m.SubjFavorite)
m.UpdateCache()
return nil
}
// AfterFind is a hooks called after querying.
func (m *Subject) AfterFind() error {
m.UpdateCache()
return nil
} }
// Deleted returns true if the entity is deleted. // Deleted returns true if the entity is deleted.
@@ -139,7 +155,7 @@ func (m *Subject) Deleted() bool {
} }
// Restore restores the entity in the database. // Restore restores the entity in the database.
func (m *Subject) Restore() error { func (m *Subject) Restore() (err error) {
if m.Deleted() { if m.Deleted() {
m.DeletedAt = nil m.DeletedAt = nil
@@ -154,20 +170,26 @@ func (m *Subject) Restore() error {
}) })
} }
return UnscopedDb().Model(m).UpdateColumn("DeletedAt", nil).Error return m.Update("DeletedAt", nil)
} }
return nil return nil
} }
// Update updates an entity value in the database. // Update updates an entity value in the database.
func (m *Subject) Update(attr string, value interface{}) error { func (m *Subject) Update(attr string, value interface{}) (err error) {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error if err = UnscopedDb().Model(m).Update(attr, value).Error; err != nil {
return err
}
return nil
} }
// Updates multiple values in the database. // Updates multiple values in the database.
func (m *Subject) Updates(values interface{}) error { func (m *Subject) Updates(values interface{}) (err error) {
return UnscopedDb().Model(m).Updates(values).Error if err = UnscopedDb().Model(m).Updates(values).Error; err != nil {
return err
}
return nil
} }
// FirstOrCreateSubject returns the existing entity, inserts a new entity or nil in case of errors. // FirstOrCreateSubject returns the existing entity, inserts a new entity or nil in case of errors.
@@ -178,9 +200,10 @@ func FirstOrCreateSubject(m *Subject) *Subject {
return nil return nil
} }
if found := FindSubjectByName(m.SubjName); found != nil { // Query cache.
return found if result := FindSubjectByName(m.SubjName); result != nil {
} else if createErr := m.Create(); createErr == nil { return result
} else if err := m.Create(); err == nil {
log.Infof("subject: added %s %s", TypeString(m.SubjType), sanitize.Log(m.SubjName)) log.Infof("subject: added %s %s", TypeString(m.SubjType), sanitize.Log(m.SubjName))
event.EntitiesCreated("subjects", []*Subject{m}) event.EntitiesCreated("subjects", []*Subject{m})
@@ -193,24 +216,27 @@ func FirstOrCreateSubject(m *Subject) *Subject {
} }
return m return m
} else if found = FindSubjectByName(m.SubjName); found != nil { } else if result := FindSubjectByName(m.SubjName); result != nil {
return found return result
} else { } else {
log.Errorf("subject: %s while creating %s", createErr, sanitize.Log(m.SubjName)) log.Errorf("subject: %s while creating %s", err, sanitize.Log(m.SubjName))
} }
return nil return nil
} }
// FindSubject returns an existing entity if exists. // FindSubject returns an existing entity if exists.
func FindSubject(s string) *Subject { func FindSubject(uid string) *Subject {
if s == "" { if uid == "" {
return nil return nil
} }
result := Subject{} result := Subject{}
if err := UnscopedDb().Where("subj_uid = ?", s).First(&result).Error; err != nil { // Find subject by uid.
if cached, ok := result.Cache().UID(uid); ok {
result = *cached.(*Subject)
} else if err := UnscopedDb().Where("subj_uid = ?", uid).First(&result).Error; err != nil {
return nil return nil
} }
@@ -227,16 +253,18 @@ func FindSubjectByName(name string) *Subject {
result := Subject{} result := Subject{}
// Search database. // Find subject by name.
if err := UnscopedDb().Where("subj_name LIKE ?", name).First(&result).Error; err != nil { if cached, ok := result.Cache().Name(name); ok && cached != nil {
result = *cached.(*Subject)
} else if err := UnscopedDb().Where("subj_name LIKE ?", name).First(&result).Error; err != nil {
return nil return nil
} }
// Restore if currently deleted. // Restore if currently deleted.
if err := result.Restore(); err != nil { if err := result.Restore(); err != nil {
log.Errorf("subject: %s could not be restored", result.SubjUID) log.Errorf("subject: %s could not be restored", LogSubj(result.SubjUID))
} else { } else {
log.Debugf("subject: %s restored", result.SubjUID) log.Debugf("subject: %s restored", LogSubj(result.SubjUID))
} }
return &result return &result
@@ -376,7 +404,7 @@ func (m *Subject) UpdateMarkerNames() error {
if err := Db().Model(&Marker{}). if err := Db().Model(&Marker{}).
Where("subj_uid = ? AND subj_src <> ?", m.SubjUID, SrcAuto). Where("subj_uid = ? AND subj_src <> ?", m.SubjUID, SrcAuto).
Where("marker_name <> ?", m.SubjName). Where("marker_name <> ?", m.SubjName).
UpdateColumn("marker_name", m.SubjName).Error; err != nil { Update("marker_name", m.SubjName).Error; err != nil {
return err return err
} }
@@ -417,11 +445,11 @@ func (m *Subject) MergeWith(other *Subject) error {
// Update markers and faces with new SubjUID. // Update markers and faces with new SubjUID.
if err := Db().Model(&Marker{}). if err := Db().Model(&Marker{}).
Where("subj_uid = ?", m.SubjUID). Where("subj_uid = ?", m.SubjUID).
UpdateColumn("subj_uid", other.SubjUID).Error; err != nil { Update("subj_uid", other.SubjUID).Error; err != nil {
return err return err
} else if err := Db().Model(&Face{}). } else if err := Db().Model(&Face{}).
Where("subj_uid = ?", m.SubjUID). Where("subj_uid = ?", m.SubjUID).
UpdateColumn("subj_uid", other.SubjUID).Error; err != nil { Update("subj_uid", other.SubjUID).Error; err != nil {
return err return err
} else if err := other.UpdateMarkerNames(); err != nil { } else if err := other.UpdateMarkerNames(); err != nil {
return err return err

View File

@@ -0,0 +1,64 @@
package entity
import "github.com/photoprism/photoprism/pkg/rnd"
var subjectCache *Cache
// CacheUID returns the subject's UID.
func (m Subject) CacheUID() string {
return m.SubjUID
}
// CacheName returns the subject's name.
func (m Subject) CacheName() string {
return m.SubjName
}
// Cached finds a cached subject.
func (m Subject) Cached(key string) *Subject {
if key == "" {
return nil
}
result := Subject{}
// Query cache.
if cached, found := m.Cache().UID(key); found && rnd.IsPPID(key, 'j') {
result = *cached.(*Subject)
} else if cached, found := m.Cache().Name(key); found {
result = *cached.(*Subject)
} else {
return nil
}
return &result
}
// UpdateCache updates cached values.
func (m Subject) UpdateCache() {
Subject{}.Cache().Set(&m)
}
// Cache returns the cache instance.
func (m Subject) Cache() *Cache {
if subjectCache != nil {
return subjectCache
}
var items []Subject
if err := UnscopedDb().Find(&items).Error; err != nil {
log.Tracef("cache: %s", err)
subjectCache = NewCache(make(Cached))
} else {
cached := make(Cached, len(items))
for _, i := range items {
cached[i.CacheUID()] = i
}
subjectCache = NewCache(cached)
}
return subjectCache
}

View File

@@ -187,6 +187,13 @@ func TestFindSubject(t *testing.T) {
r := FindSubject("") r := FindSubject("")
assert.Nil(t, r) assert.Nil(t, r)
}) })
t.Run("jqy3y652h8njw0sx", func(t *testing.T) {
r := FindSubject("jqy3y652h8njw0sx")
assert.IsType(t, &Subject{}, r)
assert.Equal(t, "jqy3y652h8njw0sx", r.SubjUID)
assert.Equal(t, "Joe Biden", r.SubjName)
assert.Equal(t, "Joe Biden", r.CacheName())
})
} }
func TestSubject_Links(t *testing.T) { func TestSubject_Links(t *testing.T) {
@@ -228,7 +235,6 @@ func TestSubject_Updates(t *testing.T) {
assert.Equal(t, "UpdatedType", m.SubjType) assert.Equal(t, "UpdatedType", m.SubjType)
} }
}) })
} }
func TestSubject_Visible(t *testing.T) { func TestSubject_Visible(t *testing.T) {

View File

@@ -2,6 +2,8 @@ package entity
import ( import (
"fmt" "fmt"
"github.com/photoprism/photoprism/pkg/sanitize"
) )
// Subjects represents a list of subjects. // Subjects represents a list of subjects.
@@ -18,6 +20,17 @@ func (m Subjects) Delete() error {
return nil return nil
} }
// LogSubj returns the sanitized subject name or id for logging.
func LogSubj(uid string) string {
cached := Subject{}.Cached(uid)
if cached != nil {
return sanitize.Log(cached.SubjName)
} else {
return sanitize.Log(uid)
}
}
// OrphanPeople returns unused subjects. // OrphanPeople returns unused subjects.
func OrphanPeople() (Subjects, error) { func OrphanPeople() (Subjects, error) {
orphans := Subjects{} orphans := Subjects{}

View File

@@ -309,7 +309,7 @@ func (m *User) InvalidPassword(password string) bool {
} }
if pw.InvalidPassword(password) { if pw.InvalidPassword(password) {
if err := Db().Model(m).UpdateColumn("login_attempts", gorm.Expr("login_attempts + ?", 1)).Error; err != nil { if err := Db().Model(m).Update("login_attempts", gorm.Expr("login_attempts + ?", 1)).Error; err != nil {
log.Errorf("user: %s (update login attempts)", err) log.Errorf("user: %s (update login attempts)", err)
} }

View File

@@ -78,13 +78,13 @@ func (w *Faces) Audit(fix bool) (err error) {
log.Infof("face %s: ambiguous subject 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 != "" { if f1.SubjUID != "" {
log.Infof("face %s: subject %s (%s %s)", f1.ID, sanitize.Log(subj[f1.SubjUID].SubjName), f1.SubjUID, entity.SrcString(f1.FaceSrc)) log.Infof("face %s: subject %s (%s %s)", f1.ID, entity.LogSubj(f1.SubjUID), f1.SubjUID, entity.SrcString(f1.FaceSrc))
} else { } else {
log.Infof("face %s: has 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 != "" { if f2.SubjUID != "" {
log.Infof("face %s: subject %s (%s %s)", f2.ID, sanitize.Log(subj[f2.SubjUID].SubjName), f2.SubjUID, entity.SrcString(f2.FaceSrc)) log.Infof("face %s: subject %s (%s %s)", f2.ID, entity.LogSubj(f2.SubjUID), f2.SubjUID, entity.SrcString(f2.FaceSrc))
} else { } else {
log.Infof("face %s: has no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc)) log.Infof("face %s: has no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
} }

View File

@@ -49,7 +49,7 @@ func (w *Faces) Optimize() (result FacesOptimizeResult, err error) {
merge = nil merge = nil
} else if ok, dist := merge[0].Match(face.Embeddings{faces[j].Embedding()}); ok { } else if ok, dist := merge[0].Match(face.Embeddings{faces[j].Embedding()}); ok {
log.Debugf("faces: can merge %s with %s, subject %s, dist %f", merge[0].ID, faces[j].ID, merge[0].SubjUID, dist) log.Debugf("faces: can merge %s with %s, subject %s, dist %f", merge[0].ID, faces[j].ID, entity.LogSubj(merge[0].SubjUID), dist)
merge = append(merge, faces[j]) merge = append(merge, faces[j])
} else if len(merge) == 1 { } else if len(merge) == 1 {
merge = nil merge = nil

View File

@@ -35,7 +35,7 @@ func UpdateAlbumDefaultCovers() (err error) {
SET thumb = b.file_hash WHERE ?`, condition) SET thumb = b.file_hash WHERE ?`, condition)
case SQLite3: case SQLite3:
res = Db().Table(entity.Album{}.TableName()). res = Db().Table(entity.Album{}.TableName()).
UpdateColumn("thumb", gorm.Expr(`( Update("thumb", gorm.Expr(`(
SELECT f.file_hash FROM files f SELECT f.file_hash FROM files f
JOIN photos_albums pa ON pa.album_uid = albums.album_uid AND pa.photo_uid = f.photo_uid AND pa.hidden = 0 AND pa.missing = 0 JOIN photos_albums pa ON pa.album_uid = albums.album_uid AND pa.photo_uid = f.photo_uid AND pa.hidden = 0 AND pa.missing = 0
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > 0 JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > 0
@@ -81,7 +81,7 @@ func UpdateAlbumFolderCovers() (err error) {
) b ON b.photo_path = albums.album_path ) b ON b.photo_path = albums.album_path
SET thumb = b.file_hash WHERE ?`, condition) SET thumb = b.file_hash WHERE ?`, condition)
case SQLite3: case SQLite3:
res = Db().Table(entity.Album{}.TableName()).UpdateColumn("thumb", gorm.Expr(`( res = Db().Table(entity.Album{}.TableName()).Update("thumb", gorm.Expr(`(
SELECT f.file_hash FROM files f,( SELECT f.file_hash FROM files f,(
SELECT p.photo_path, max(p.id) AS photo_id FROM photos p SELECT p.photo_path, max(p.id) AS photo_id FROM photos p
WHERE p.photo_quality > 0 AND p.photo_private = 0 AND p.deleted_at IS NULL WHERE p.photo_quality > 0 AND p.photo_private = 0 AND p.deleted_at IS NULL
@@ -129,7 +129,7 @@ func UpdateAlbumMonthCovers() (err error) {
) b ON b.photo_year = albums.album_year AND b.photo_month = albums.album_month ) b ON b.photo_year = albums.album_year AND b.photo_month = albums.album_month
SET thumb = b.file_hash WHERE ?`, condition) SET thumb = b.file_hash WHERE ?`, condition)
case SQLite3: case SQLite3:
res = Db().Table(entity.Album{}.TableName()).UpdateColumn("thumb", gorm.Expr(`( res = Db().Table(entity.Album{}.TableName()).Update("thumb", gorm.Expr(`(
SELECT f.file_hash FROM files f,( SELECT f.file_hash FROM files f,(
SELECT p.photo_year, p.photo_month, max(p.id) AS photo_id FROM photos p SELECT p.photo_year, p.photo_month, max(p.id) AS photo_id FROM photos p
WHERE p.photo_quality > 0 AND p.photo_private = 0 AND p.deleted_at IS NULL WHERE p.photo_quality > 0 AND p.photo_private = 0 AND p.deleted_at IS NULL
@@ -205,7 +205,7 @@ func UpdateLabelCovers() (err error) {
) b ON b.label_id = labels.id ) b ON b.label_id = labels.id
SET thumb = b.file_hash WHERE ?`, condition) SET thumb = b.file_hash WHERE ?`, condition)
case SQLite3: case SQLite3:
res = Db().Table(entity.Label{}.TableName()).UpdateColumn("thumb", gorm.Expr(`( res = Db().Table(entity.Label{}.TableName()).Update("thumb", gorm.Expr(`(
SELECT f.file_hash FROM files f SELECT f.file_hash FROM files f
JOIN photos_labels pl ON pl.label_id = labels.id AND pl.photo_id = f.photo_id AND pl.uncertainty < 100 JOIN photos_labels pl ON pl.label_id = labels.id AND pl.photo_id = f.photo_id AND pl.uncertainty < 100
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > 0 JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > 0
@@ -214,7 +214,7 @@ func UpdateLabelCovers() (err error) {
) WHERE ?`, condition)) ) WHERE ?`, condition))
if res.Error == nil { if res.Error == nil {
catRes := Db().Table(entity.Label{}.TableName()).UpdateColumn("thumb", gorm.Expr(`( catRes := Db().Table(entity.Label{}.TableName()).Update("thumb", gorm.Expr(`(
SELECT f.file_hash FROM files f SELECT f.file_hash FROM files f
JOIN photos_labels pl ON pl.photo_id = f.photo_id AND pl.uncertainty < 100 JOIN photos_labels pl ON pl.photo_id = f.photo_id AND pl.uncertainty < 100
JOIN categories c ON c.label_id = pl.label_id AND c.category_id = labels.id JOIN categories c ON c.label_id = pl.label_id AND c.category_id = labels.id
@@ -271,7 +271,7 @@ func UpdateSubjectCovers() (err error) {
SET thumb = marker_thumb WHERE ?`, gorm.Expr(subjTable), gorm.Expr(markerTable), condition) SET thumb = marker_thumb WHERE ?`, gorm.Expr(subjTable), gorm.Expr(markerTable), condition)
case SQLite3: case SQLite3:
from := gorm.Expr(fmt.Sprintf("%s m WHERE m.subj_uid = %s.subj_uid ", markerTable, subjTable)) from := gorm.Expr(fmt.Sprintf("%s m WHERE m.subj_uid = %s.subj_uid ", markerTable, subjTable))
res = Db().Table(entity.Subject{}.TableName()).UpdateColumn("thumb", gorm.Expr(`( res = Db().Table(entity.Subject{}.TableName()).Update("thumb", gorm.Expr(`(
SELECT m.thumb FROM ? AND m.thumb <> '' ORDER BY m.subj_src DESC, m.q DESC LIMIT 1 SELECT m.thumb FROM ? AND m.thumb <> '' ORDER BY m.subj_src DESC, m.q DESC LIMIT 1
) WHERE ?`, from, condition)) ) WHERE ?`, from, condition))
default: default:

View File

@@ -6,7 +6,6 @@ import (
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/face" "github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/pkg/sanitize"
) )
// Faces returns all (known / unmatched) faces from the index. // Faces returns all (known / unmatched) faces from the index.
@@ -53,7 +52,7 @@ func MatchFaceMarkers() (affected int64, err error) {
Where("face_id = ?", f.ID). Where("face_id = ?", f.ID).
Where("subj_src = ?", entity.SrcAuto). Where("subj_src = ?", entity.SrcAuto).
Where("subj_uid <> ?", f.SubjUID). Where("subj_uid <> ?", f.SubjUID).
UpdateColumns(entity.Values{"subj_uid": f.SubjUID, "marker_review": false}); res.Error != nil { Updates(entity.Values{"subj_uid": f.SubjUID, "marker_review": false}); res.Error != nil {
return affected, err return affected, err
} else if res.RowsAffected > 0 { } else if res.RowsAffected > 0 {
affected += res.RowsAffected affected += res.RowsAffected
@@ -138,15 +137,15 @@ func MergeFaces(merge entity.Faces) (merged *entity.Face, err error) {
for i := 1; i < len(merge); i++ { for i := 1; i < len(merge); i++ {
if merge[i].SubjUID != subjUID { if merge[i].SubjUID != subjUID {
return merged, fmt.Errorf("faces: cannot merge clusters with conflicting subjects %s <> %s", return merged, fmt.Errorf("faces: cannot merge clusters with conflicting subjects %s <> %s",
sanitize.Log(subjUID), sanitize.Log(merge[i].SubjUID)) LogSubj(subjUID), LogSubj(merge[i].SubjUID))
} }
} }
// Find or create merged face cluster. // Find or create merged face cluster.
if merged = entity.NewFace(merge[0].SubjUID, merge[0].FaceSrc, merge.Embeddings()); merged == nil { if merged = entity.NewFace(merge[0].SubjUID, merge[0].FaceSrc, merge.Embeddings()); merged == nil {
return merged, fmt.Errorf("faces: new cluster is nil for subject %s", sanitize.Log(subjUID)) return merged, fmt.Errorf("faces: new cluster is nil for subject %s", LogSubj(subjUID))
} else if merged = entity.FirstOrCreateFace(merged); merged == nil { } else if merged = entity.FirstOrCreateFace(merged); merged == nil {
return merged, fmt.Errorf("faces: failed creating new cluster for subject %s", sanitize.Log(subjUID)) return merged, fmt.Errorf("faces: failed creating new cluster for subject %s", LogSubj(subjUID))
} else if err := merged.MatchMarkers(append(merge.IDs(), "")); err != nil { } else if err := merged.MatchMarkers(append(merge.IDs(), "")); err != nil {
return merged, err return merged, err
} }
@@ -155,9 +154,9 @@ func MergeFaces(merge entity.Faces) (merged *entity.Face, err error) {
if removed, err := PurgeOrphanFaces(merge.IDs()); err != nil { if removed, err := PurgeOrphanFaces(merge.IDs()); err != nil {
return merged, err return merged, err
} else if removed > 0 { } else if removed > 0 {
log.Debugf("faces: removed %d orphans for subject %s", removed, sanitize.Log(subjUID)) log.Debugf("faces: removed %d orphans for subject %s", removed, LogSubj(subjUID))
} else { } else {
log.Warnf("faces: failed removing merged clusters for subject %s", sanitize.Log(subjUID)) log.Warnf("faces: failed removing merged clusters for subject %s", LogSubj(subjUID))
} }
return merged, err return merged, err
@@ -185,13 +184,13 @@ func ResolveFaceCollisions() (conflicts, resolved int, err error) {
log.Infof("face %s: ambiguous subject 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 != "" { if f1.SubjUID != "" {
log.Debugf("face %s: subject %s (%s %s)", f1.ID, sanitize.Log(f1.SubjUID), f1.SubjUID, entity.SrcString(f1.FaceSrc)) log.Debugf("face %s: subject %s (%s %s)", f1.ID, entity.LogSubj(f1.SubjUID), f1.SubjUID, entity.SrcString(f1.FaceSrc))
} else { } else {
log.Debugf("face %s: has 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 != "" { if f2.SubjUID != "" {
log.Debugf("face %s: subject %s (%s %s)", f2.ID, sanitize.Log(f2.SubjUID), f2.SubjUID, entity.SrcString(f2.FaceSrc)) log.Debugf("face %s: subject %s (%s %s)", f2.ID, entity.LogSubj(f2.SubjUID), f2.SubjUID, entity.SrcString(f2.FaceSrc))
} else { } else {
log.Debugf("face %s: has no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc)) log.Debugf("face %s: has no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
} }
@@ -233,7 +232,7 @@ func RemovePeopleAndFaces() (err error) {
// Reset face counters. // Reset face counters.
if err = UnscopedDb().Model(entity.Photo{}). if err = UnscopedDb().Model(entity.Photo{}).
UpdateColumn("photo_faces", 0).Error; err != nil { Update("photo_faces", 0).Error; err != nil {
return err return err
} }

View File

@@ -89,7 +89,7 @@ func TestMatchFaceMarkers(t *testing.T) {
if err := Db().Model(&entity.Marker{}). if err := Db().Model(&entity.Marker{}).
Where("subj_src = ?", entity.SrcAuto). Where("subj_src = ?", entity.SrcAuto).
Where("subj_uid = ?", "jqu0xs11qekk9jx8"). Where("subj_uid = ?", "jqu0xs11qekk9jx8").
UpdateColumn("subj_uid", "").Error; err != nil { Update("subj_uid", "").Error; err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -125,11 +125,11 @@ func SetPhotoPrimary(photoUID, fileUID string) (err error) {
if err = Db().Model(entity.File{}). if err = Db().Model(entity.File{}).
Where("photo_uid = ? AND file_uid <> ?", photoUID, fileUID). Where("photo_uid = ? AND file_uid <> ?", photoUID, fileUID).
UpdateColumn("file_primary", 0).Error; err != nil { Update("file_primary", 0).Error; err != nil {
return err return err
} else if err = Db(). } else if err = Db().
Model(entity.File{}).Where("photo_uid = ? AND file_uid = ?", photoUID, fileUID). Model(entity.File{}).Where("photo_uid = ? AND file_uid = ?", photoUID, fileUID).
UpdateColumn("file_primary", 1).Error; err != nil { Update("file_primary", 1).Error; err != nil {
return err return err
} else { } else {
entity.File{PhotoUID: photoUID}.RegenerateIndex() entity.File{PhotoUID: photoUID}.RegenerateIndex()
@@ -140,7 +140,7 @@ func SetPhotoPrimary(photoUID, fileUID string) (err error) {
// SetFileError updates the file error column. // SetFileError updates the file error column.
func SetFileError(fileUID, errorString string) { func SetFileError(fileUID, errorString string) {
if err := Db().Model(entity.File{}).Where("file_uid = ?", fileUID).UpdateColumn("file_error", errorString).Error; err != nil { if err := Db().Model(entity.File{}).Where("file_uid = ?", fileUID).Update("file_error", errorString).Error; err != nil {
log.Errorf("files: %s (set error)", err.Error()) log.Errorf("files: %s (set error)", err.Error())
} }
} }

View File

@@ -125,7 +125,7 @@ func RemoveInvalidMarkerReferences() (removed int64, err error) {
res := Db(). res := Db().
Model(&entity.Marker{}). Model(&entity.Marker{}).
Where("marker_invalid = 1 AND (subj_uid <> '' OR face_id <> '')"). Where("marker_invalid = 1 AND (subj_uid <> '' OR face_id <> '')").
UpdateColumns(entity.Values{"subj_uid": "", "face_id": "", "face_dist": -1.0, "matched_at": nil}) Updates(entity.Values{"subj_uid": "", "face_id": "", "face_dist": -1.0, "matched_at": nil})
return res.RowsAffected, res.Error return res.RowsAffected, res.Error
} }
@@ -137,7 +137,7 @@ func RemoveNonExistentMarkerFaces() (removed int64, err error) {
Model(&entity.Marker{}). Model(&entity.Marker{}).
Where("marker_type = ?", entity.MarkerFace). Where("marker_type = ?", entity.MarkerFace).
Where(fmt.Sprintf("face_id <> '' AND face_id NOT IN (SELECT id FROM %s)", entity.Face{}.TableName())). Where(fmt.Sprintf("face_id <> '' AND face_id NOT IN (SELECT id FROM %s)", entity.Face{}.TableName())).
UpdateColumns(entity.Values{"face_id": "", "face_dist": -1.0, "matched_at": nil}) Updates(entity.Values{"face_id": "", "face_dist": -1.0, "matched_at": nil})
return res.RowsAffected, res.Error return res.RowsAffected, res.Error
} }
@@ -147,7 +147,7 @@ func RemoveNonExistentMarkerSubjects() (removed int64, err error) {
res := Db(). res := Db().
Model(&entity.Marker{}). Model(&entity.Marker{}).
Where(fmt.Sprintf("subj_uid <> '' AND subj_uid NOT IN (SELECT subj_uid FROM %s)", entity.Subject{}.TableName())). Where(fmt.Sprintf("subj_uid <> '' AND subj_uid NOT IN (SELECT subj_uid FROM %s)", entity.Subject{}.TableName())).
UpdateColumns(entity.Values{"subj_uid": "", "matched_at": nil}) Updates(entity.Values{"subj_uid": "", "matched_at": nil})
return res.RowsAffected, res.Error return res.RowsAffected, res.Error
} }
@@ -209,7 +209,7 @@ func MarkersWithSubjectConflict() (results entity.Markers, err error) {
func ResetFaceMarkerMatches() (removed int64, err error) { func ResetFaceMarkerMatches() (removed int64, err error) {
res := Db().Model(&entity.Marker{}). res := Db().Model(&entity.Marker{}).
Where("subj_src = ? AND marker_type = ?", entity.SrcAuto, entity.MarkerFace). Where("subj_src = ? AND marker_type = ?", entity.SrcAuto, entity.MarkerFace).
UpdateColumns(entity.Values{"marker_name": "", "subj_uid": "", "subj_src": "", "face_id": "", "face_dist": -1.0, "matched_at": nil}) Updates(entity.Values{"marker_name": "", "subj_uid": "", "subj_src": "", "face_id": "", "face_dist": -1.0, "matched_at": nil})
return res.RowsAffected, res.Error return res.RowsAffected, res.Error
} }

View File

@@ -128,7 +128,7 @@ func FixPrimaries() error {
// Remove primary file flag from broken or missing files. // Remove primary file flag from broken or missing files.
if err := UnscopedDb().Table(entity.File{}.TableName()). if err := UnscopedDb().Table(entity.File{}.TableName()).
Where("file_error <> '' OR file_missing = 1"). Where("file_error <> '' OR file_missing = 1").
UpdateColumn("file_primary", 0).Error; err != nil { Update("file_primary", 0).Error; err != nil {
return err return err
} }

View File

@@ -72,6 +72,11 @@ func RemoveOrphanSubjects() (removed int64, err error) {
return res.RowsAffected, res.Error return res.RowsAffected, res.Error
} }
// LogSubj returns the sanitized subject name or id for logging.
func LogSubj(uid string) string {
return entity.LogSubj(uid)
}
// CreateMarkerSubjects adds and references known marker subjects. // CreateMarkerSubjects adds and references known marker subjects.
func CreateMarkerSubjects() (affected int64, err error) { func CreateMarkerSubjects() (affected int64, err error) {
var markers entity.Markers var markers entity.Markers