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)
AbortSaveFailed(c)
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)
}
@@ -124,7 +124,7 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
}
}
} 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)
AbortSaveFailed(c)
return
@@ -260,7 +260,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
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 {
log.Errorf("private: %s", err)
AbortSaveFailed(c)

View File

@@ -211,12 +211,12 @@ func (m *Account) Directories() (result fs.FileInfos, err error) {
// Updates multiple columns in the database.
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.
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.

View File

@@ -483,12 +483,12 @@ func (m *Album) SaveForm(f form.Album) error {
// Update sets a new value for a database column.
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.
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.
@@ -500,7 +500,7 @@ func (m *Album) UpdateFolder(albumPath, albumFilter string) error {
return nil
}
if err := UnscopedDb().Model(m).UpdateColumns(map[string]interface{}{
if err := UnscopedDb().Model(m).Updates(map[string]interface{}{
"AlbumPath": albumPath,
"AlbumFilter": albumFilter,
"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)
} else if err := UnscopedDb().Table(Photo{}.TableName()).
Where("place_id = ?", oldPlaceID).
UpdateColumn("place_id", m.PlaceID).
Update("place_id", m.PlaceID).
Error; err != nil {
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.
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 "+
"AND p.photo_quality >= 0 "+
"AND p.photo_private = 0 "+
@@ -99,7 +99,7 @@ func UpdateSubjectCounts() (err error) {
case SQLite3:
// Update files count.
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 ",
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
} else {
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 ",
markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition))
res.RowsAffected += photosRes.RowsAffected
@@ -150,7 +150,7 @@ func UpdateLabelCounts() (err error) {
} else if IsDialect(SQLite3) {
res = Db().
Table("labels").
UpdateColumn("photo_count",
Update("photo_count",
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
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.
func (m *Face) Matched() error {
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.
@@ -166,7 +166,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.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.
m.SubjUID = ""
@@ -260,7 +260,7 @@ func (m *Face) SetSubjectUID(subjUID string) (err error) {
Where("subj_src = ?", SrcAuto).
Where("subj_uid <> ?", m.SubjUID).
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
}
@@ -319,7 +319,7 @@ func (m *Face) Delete() error {
// Remove face id from markers before deleting.
if err := Db().Model(&Marker{}).
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
}
@@ -341,12 +341,14 @@ func FirstOrCreateFace(m *Face) *Face {
result := Face{}
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
} else if createErr := m.Create(); createErr == nil {
return m
} 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
} else {
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) {
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}})
r := FirstOrCreateFace(m)
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) {
m := FaceFixtures.Pointer("joe-biden")
r := FirstOrCreateFace(m)

View File

@@ -331,7 +331,7 @@ func (m *File) ReplaceHash(newHash string) error {
for name, entity := range entities {
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
} else if res.RowsAffected > 0 {
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.
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.
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.
@@ -644,7 +644,7 @@ func (m *File) UpdatePhotoFaceCount() (c int, err error) {
err = UnscopedDb().Model(Photo{}).
Where("id = ?", m.PhotoID).
UpdateColumn("photo_faces", c).Error
Update("photo_faces", c).Error
return c, err
}

View File

@@ -46,12 +46,12 @@ func NewFileShare(fileID, accountID uint, remoteName string) *FileShare {
// Updates multiple columns in the database.
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.
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.

View File

@@ -46,12 +46,12 @@ func NewFileSync(accountID uint, remoteName string) *FileSync {
// Updates multiple columns in the database.
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.
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.

View File

@@ -571,7 +571,18 @@ func TestFile_SubjectNames(t *testing.T) {
t.Run("Video.jpg", func(t *testing.T) {
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()
t.Log(len(names))
if len(names) != 1 {
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.
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.
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.

View File

@@ -115,7 +115,7 @@ func (m *Label) Restore() error {
// Update a label property in the database.
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.

View File

@@ -58,7 +58,7 @@ func NewLink(shareUID string, canComment, canEdit bool) Link {
func (m *Link) Redeem() {
m.LinkViews += 1
result := Db().Model(m).UpdateColumn("LinkViews", m.LinkViews)
result := Db().Model(m).Update("LinkViews", m.LinkViews)
if result.RowsAffected == 0 {
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 == "" {
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)
return false
} else {
@@ -342,7 +342,7 @@ func (m *Marker) SyncSubject(updateRelated bool) (err error) {
// Update related markers?
if m.FaceID == "" || m.SubjUID == "" {
// 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)
} else if !updateRelated {
return nil
@@ -351,7 +351,7 @@ func (m *Marker) SyncSubject(updateRelated bool) (err error) {
Where("face_id = ?", m.FaceID).
Where("subj_src = ?", SrcAuto).
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)
} else if res.RowsAffected > 0 && m.face != nil {
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.
func (m *Marker) Matched() error {
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.

View File

@@ -697,7 +697,7 @@ func (m *Photo) AllFiles() (files Files) {
func (m *Photo) Archive() error {
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
} else if err := m.Update("deleted_at", deletedAt); err != nil {
return err
@@ -780,12 +780,12 @@ func (m *Photo) NoDescription() bool {
// Update a column in the database.
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.
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.
@@ -886,10 +886,10 @@ func (m *Photo) SetPrimary(fileUID string) (err error) {
if err = Db().Model(File{}).
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
} 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
} else if 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.
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.
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.

View File

@@ -98,7 +98,7 @@ func (m *Subject) Create() error {
}
// Delete marks the entity as deleted in the database.
func (m *Subject) Delete() error {
func (m *Subject) Delete() (err error) {
if m.Deleted() {
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 Db().Delete(m).Error
err = Db().Delete(m).Error
return err
}
// AfterDelete resets file and photo counters when the entity was deleted.
func (m *Subject) AfterDelete(tx *gorm.DB) (err error) {
tx.Model(m).Updates(Values{
func (m *Subject) AfterDelete(db *gorm.DB) (err error) {
err = db.Model(m).Updates(Values{
"FileCount": 0,
"PhotoCount": 0,
})
return
}).Error
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.
@@ -139,7 +155,7 @@ func (m *Subject) Deleted() bool {
}
// Restore restores the entity in the database.
func (m *Subject) Restore() error {
func (m *Subject) Restore() (err error) {
if m.Deleted() {
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
}
// Update updates an entity value in the database.
func (m *Subject) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
func (m *Subject) Update(attr string, value interface{}) (err error) {
if err = UnscopedDb().Model(m).Update(attr, value).Error; err != nil {
return err
}
return nil
}
// Updates multiple values in the database.
func (m *Subject) Updates(values interface{}) error {
return UnscopedDb().Model(m).Updates(values).Error
func (m *Subject) Updates(values interface{}) (err 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.
@@ -178,9 +200,10 @@ func FirstOrCreateSubject(m *Subject) *Subject {
return nil
}
if found := FindSubjectByName(m.SubjName); found != nil {
return found
} else if createErr := m.Create(); createErr == nil {
// Query cache.
if result := FindSubjectByName(m.SubjName); result != nil {
return result
} else if err := m.Create(); err == nil {
log.Infof("subject: added %s %s", TypeString(m.SubjType), sanitize.Log(m.SubjName))
event.EntitiesCreated("subjects", []*Subject{m})
@@ -193,24 +216,27 @@ func FirstOrCreateSubject(m *Subject) *Subject {
}
return m
} else if found = FindSubjectByName(m.SubjName); found != nil {
return found
} else if result := FindSubjectByName(m.SubjName); result != nil {
return result
} 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
}
// FindSubject returns an existing entity if exists.
func FindSubject(s string) *Subject {
if s == "" {
func FindSubject(uid string) *Subject {
if uid == "" {
return nil
}
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
}
@@ -227,16 +253,18 @@ func FindSubjectByName(name string) *Subject {
result := Subject{}
// Search database.
if err := UnscopedDb().Where("subj_name LIKE ?", name).First(&result).Error; err != nil {
// Find subject by name.
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
}
// Restore if currently deleted.
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 {
log.Debugf("subject: %s restored", result.SubjUID)
log.Debugf("subject: %s restored", LogSubj(result.SubjUID))
}
return &result
@@ -376,7 +404,7 @@ func (m *Subject) UpdateMarkerNames() error {
if err := Db().Model(&Marker{}).
Where("subj_uid = ? AND subj_src <> ?", m.SubjUID, SrcAuto).
Where("marker_name <> ?", m.SubjName).
UpdateColumn("marker_name", m.SubjName).Error; err != nil {
Update("marker_name", m.SubjName).Error; err != nil {
return err
}
@@ -417,11 +445,11 @@ func (m *Subject) MergeWith(other *Subject) error {
// Update markers and faces with new SubjUID.
if err := Db().Model(&Marker{}).
Where("subj_uid = ?", m.SubjUID).
UpdateColumn("subj_uid", other.SubjUID).Error; err != nil {
Update("subj_uid", other.SubjUID).Error; err != nil {
return err
} else if err := Db().Model(&Face{}).
Where("subj_uid = ?", m.SubjUID).
UpdateColumn("subj_uid", other.SubjUID).Error; err != nil {
Update("subj_uid", other.SubjUID).Error; err != nil {
return err
} else if err := other.UpdateMarkerNames(); err != nil {
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("")
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) {
@@ -228,7 +235,6 @@ func TestSubject_Updates(t *testing.T) {
assert.Equal(t, "UpdatedType", m.SubjType)
}
})
}
func TestSubject_Visible(t *testing.T) {

View File

@@ -2,6 +2,8 @@ package entity
import (
"fmt"
"github.com/photoprism/photoprism/pkg/sanitize"
)
// Subjects represents a list of subjects.
@@ -18,6 +20,17 @@ func (m Subjects) Delete() error {
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.
func OrphanPeople() (Subjects, error) {
orphans := Subjects{}

View File

@@ -309,7 +309,7 @@ func (m *User) InvalidPassword(password string) bool {
}
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)
}

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)
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 {
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, 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 {
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
} 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])
} else if len(merge) == 1 {
merge = nil

View File

@@ -35,7 +35,7 @@ func UpdateAlbumDefaultCovers() (err error) {
SET thumb = b.file_hash WHERE ?`, condition)
case SQLite3:
res = Db().Table(entity.Album{}.TableName()).
UpdateColumn("thumb", gorm.Expr(`(
Update("thumb", gorm.Expr(`(
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 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
SET thumb = b.file_hash WHERE ?`, condition)
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 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
@@ -129,7 +129,7 @@ func UpdateAlbumMonthCovers() (err error) {
) b ON b.photo_year = albums.album_year AND b.photo_month = albums.album_month
SET thumb = b.file_hash WHERE ?`, condition)
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 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
@@ -205,7 +205,7 @@ func UpdateLabelCovers() (err error) {
) b ON b.label_id = labels.id
SET thumb = b.file_hash WHERE ?`, condition)
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
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
@@ -214,7 +214,7 @@ func UpdateLabelCovers() (err error) {
) WHERE ?`, condition))
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
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
@@ -271,7 +271,7 @@ func UpdateSubjectCovers() (err error) {
SET thumb = marker_thumb WHERE ?`, gorm.Expr(subjTable), gorm.Expr(markerTable), condition)
case SQLite3:
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
) WHERE ?`, from, condition))
default:

View File

@@ -6,7 +6,6 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/pkg/sanitize"
)
// 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("subj_src = ?", entity.SrcAuto).
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
} else if res.RowsAffected > 0 {
affected += res.RowsAffected
@@ -138,15 +137,15 @@ func MergeFaces(merge entity.Faces) (merged *entity.Face, err error) {
for i := 1; i < len(merge); i++ {
if merge[i].SubjUID != subjUID {
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.
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 {
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 {
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 {
return merged, err
} 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 {
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
@@ -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)
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 {
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, 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 {
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.
if err = UnscopedDb().Model(entity.Photo{}).
UpdateColumn("photo_faces", 0).Error; err != nil {
Update("photo_faces", 0).Error; err != nil {
return err
}

View File

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

View File

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

View File

@@ -125,7 +125,7 @@ func RemoveInvalidMarkerReferences() (removed int64, err error) {
res := Db().
Model(&entity.Marker{}).
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
}
@@ -137,7 +137,7 @@ func RemoveNonExistentMarkerFaces() (removed int64, err error) {
Model(&entity.Marker{}).
Where("marker_type = ?", entity.MarkerFace).
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
}
@@ -147,7 +147,7 @@ func RemoveNonExistentMarkerSubjects() (removed int64, err error) {
res := Db().
Model(&entity.Marker{}).
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
}
@@ -209,7 +209,7 @@ func MarkersWithSubjectConflict() (results entity.Markers, err error) {
func ResetFaceMarkerMatches() (removed int64, err error) {
res := Db().Model(&entity.Marker{}).
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
}

View File

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

View File

@@ -72,6 +72,11 @@ func RemoveOrphanSubjects() (removed int64, err 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.
func CreateMarkerSubjects() (affected int64, err error) {
var markers entity.Markers