mirror of
https://github.com/photoprism/photoprism.git
synced 2025-10-08 02:03:33 +08:00
@@ -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)
|
||||
|
@@ -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.
|
||||
|
@@ -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
160
internal/entity/cache.go
Normal 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
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
@@ -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)
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
@@ -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)
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
64
internal/entity/subject_cache.go
Normal file
64
internal/entity/subject_cache.go
Normal 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
|
||||
}
|
@@ -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) {
|
||||
|
@@ -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{}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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))
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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())
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user