Index: Set labels based on matching keywords in title or subject #4602

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2024-11-01 12:15:39 +01:00
parent 6c5f7fac22
commit 924ddcf2cd
37 changed files with 399 additions and 114 deletions

View File

@@ -6,5 +6,7 @@ const (
SrcManual = "manual"
SrcLocation = "location"
SrcImage = "image"
SrcTitle = "title"
SrcSubject = "subject"
SrcKeyword = "keyword"
)

View File

@@ -34,10 +34,13 @@ func (l Labels) AppendLabel(label Label) Labels {
return append(l, label)
}
// Keywords returns all keywords contains in Labels and their categories
// Keywords returns all keywords contained in Labels and their categories.
func (l Labels) Keywords() (result []string) {
for _, label := range l {
if label.Uncertainty >= 100 || label.Source == SrcKeyword {
if label.Uncertainty >= 100 ||
label.Source == SrcTitle ||
label.Source == SrcSubject ||
label.Source == SrcKeyword {
continue
}

View File

@@ -138,7 +138,10 @@ func RemovePhotoLabel(router *gin.RouterGroup) {
return
}
if label.LabelSrc == classify.SrcManual || label.LabelSrc == classify.SrcKeyword {
if label.LabelSrc == classify.SrcManual ||
label.LabelSrc == classify.SrcTitle ||
label.LabelSrc == classify.SrcSubject ||
label.LabelSrc == classify.SrcKeyword {
logErr("label", entity.Db().Delete(&label).Error)
} else {
label.Uncertainty = 100

View File

@@ -64,13 +64,13 @@ type Album struct {
Photos PhotoAlbums `gorm:"foreignkey:AlbumUID;association_foreignkey:AlbumUID;" json:"-" yaml:"Photos,omitempty"`
}
// AfterUpdate flushes the album cache.
// AfterUpdate flushes the album cache when an album is updated.
func (m *Album) AfterUpdate(tx *gorm.DB) (err error) {
FlushAlbumCache()
return
}
// AfterDelete flushes the album cache when an album gets deleted.
// AfterDelete flushes the album cache when an album is deleted.
func (m *Album) AfterDelete(tx *gorm.DB) (err error) {
FlushAlbumCache()
return

View File

@@ -17,7 +17,7 @@ func FlushAlbumCache() {
albumCache.Flush()
}
// CachedAlbumByUID returns an existing album or nil if not found.
// CachedAlbumByUID returns an existing album or an error if not found.
func CachedAlbumByUID(uid string) (m Album, err error) {
// Valid album UID?
if uid == "" || rnd.InvalidUID(uid, AlbumUID) {

View File

@@ -150,7 +150,7 @@ func TestDetails_Create(t *testing.T) {
assert.Error(t, details.Create())
})
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
details := Details{PhotoID: 1236799955432}
err := details.Create()
@@ -163,7 +163,7 @@ func TestDetails_Create(t *testing.T) {
// TODO fails on mariadb
func TestDetails_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
details := Details{PhotoID: 123678955432, UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC)}
initialDate := details.UpdatedAt

View File

@@ -8,7 +8,7 @@ import (
)
func TestAddDuplicate(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
if err := AddDuplicate(
"foobar.jpg",
RootOriginals,
@@ -156,7 +156,7 @@ func TestSaveDuplicate(t *testing.T) {
}
func TestDuplicate_Purge(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
if err := AddDuplicate(
"forpurge.jpg",
RootOriginals,

View File

@@ -131,7 +131,7 @@ func TestFace_ReviseMatches(t *testing.T) {
}
func TestNewFace(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
marker := MarkerFixtures.Get("1000003-4")
e := marker.Embeddings()
@@ -165,7 +165,7 @@ func TestFace_Unsuitable(t *testing.T) {
}
func TestFace_SetEmbeddings(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
marker := MarkerFixtures.Get("1000003-4")
e := marker.Embeddings()
m := FaceFixtures.Get("joe-biden")
@@ -180,7 +180,7 @@ func TestFace_SetEmbeddings(t *testing.T) {
}
func TestFace_Embedding(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
m := FaceFixtures.Get("joe-biden")
assert.Equal(t, 0.10730543085474682, m.Embedding()[0])

View File

@@ -57,7 +57,7 @@ func TestFirstOrCreateFileShare(t *testing.T) {
}
func TestFileShare_Updates(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
fileShare := NewFileShare(123, 123, "NameBeforeUpdate")
assert.Equal(t, "NameBeforeUpdate", fileShare.RemoteName)
@@ -74,7 +74,7 @@ func TestFileShare_Updates(t *testing.T) {
}
func TestFileShare_Update(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
fileShare := NewFileShare(123, 123, "NameBeforeUpdate2")
assert.Equal(t, "NameBeforeUpdate2", fileShare.RemoteName)
assert.Equal(t, uint(0x7b), fileShare.ServiceID)
@@ -90,7 +90,7 @@ func TestFileShare_Update(t *testing.T) {
}
func TestFileShare_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
fileShare := NewFileShare(123, 123, "Nameavc")
initialDate := fileShare.UpdatedAt

View File

@@ -56,7 +56,7 @@ func TestFirstOrCreateFileSync(t *testing.T) {
}
func TestFileSync_Updates(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
fileSync := NewFileSync(123, "NameBeforeUpdate")
assert.Equal(t, "NameBeforeUpdate", fileSync.RemoteName)
@@ -73,7 +73,7 @@ func TestFileSync_Updates(t *testing.T) {
}
func TestFileSync_Update(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
fileSync := NewFileSync(123, "NameBeforeUpdate2")
assert.Equal(t, "NameBeforeUpdate2", fileSync.RemoteName)
assert.Equal(t, uint(0x7b), fileSync.ServiceID)
@@ -89,7 +89,7 @@ func TestFileSync_Update(t *testing.T) {
}
func TestFileSync_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
fileSync := NewFileSync(123, "Nameavc")
initialDate := fileSync.UpdatedAt

View File

@@ -156,7 +156,7 @@ func TestFile_Create(t *testing.T) {
assert.Nil(t, file.Create())
assert.Error(t, file.Create())
})
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: "Berlin / Morning Mood"}
file := &File{Photo: photo, FileType: "jpg", FileSize: 500, PhotoID: 766, FileName: "testname", FileRoot: "xyz"}
@@ -170,14 +170,14 @@ func TestFile_Create(t *testing.T) {
}
func TestFile_Purge(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
file := &File{Photo: nil, FileType: "jpg", FileSize: 500}
assert.Equal(t, nil, file.Purge())
})
}
func TestFile_Found(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
file := &File{Photo: nil, FileType: "jpg", FileSize: 500}
assert.Equal(t, nil, file.Purge())
assert.Equal(t, true, file.FileMissing)
@@ -216,7 +216,7 @@ func TestFile_Save(t *testing.T) {
assert.Equal(t, "file 123: cannot save file with empty photo id", err.Error())
})
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: "Berlin / Morning Mood"}
file := &File{Photo: photo, FileType: "jpg", FileSize: 500, PhotoID: 766, FileName: "Food", FileRoot: "", UpdatedAt: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC)}
@@ -230,7 +230,7 @@ func TestFile_Save(t *testing.T) {
}
func TestFile_UpdateVideoInfos(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
file := &File{FileType: "jpg", FileWidth: 600, FileName: "VideoUpdate", PhotoID: 1000003}
assert.Equal(t, "1990/04/bridge2.mp4", FileFixturesExampleBridgeVideo.FileName)
@@ -258,7 +258,7 @@ func TestFile_UpdateVideoInfos(t *testing.T) {
}
func TestFile_Update(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
file := &File{FileType: "jpg", FileSize: 500, FileName: "ToBeUpdated", FileRoot: "", PhotoID: 5678}
err := file.Save()
@@ -476,7 +476,7 @@ func TestFile_DownloadName(t *testing.T) {
}
func TestFile_Undelete(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
file := &File{Photo: nil, FileType: "jpg", FileSize: 500}
assert.Equal(t, nil, file.Purge())
assert.Equal(t, true, file.FileMissing)
@@ -554,7 +554,7 @@ func TestFile_ValidFaceCount(t *testing.T) {
}
func TestFile_Rename(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
m := FileFixtures.Get("exampleFileName.jpg")
assert.Equal(t, "2790/07/27900704_070228_D6D51B6C.jpg", m.FileName)

View File

@@ -160,7 +160,7 @@ func TestFindFolder(t *testing.T) {
}
func TestFolder_Updates(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
folder := NewFolder("oldRoot", "oldPath", time.Now().UTC())
assert.Equal(t, "oldRoot", folder.Root)
@@ -177,7 +177,7 @@ func TestFolder_Updates(t *testing.T) {
}
func TestFolder_SetForm(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
formValues := Folder{FolderTitle: "Beautiful beach"}
folderForm, err := form.NewFolder(formValues)
@@ -200,7 +200,7 @@ func TestFolder_SetForm(t *testing.T) {
}
func TestFolder_Create(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
folder := Folder{FolderTitle: "Holiday 2020", Root: RootOriginals, Path: "2020/Greece"}
err := folder.Create()
if err != nil {

View File

@@ -33,7 +33,7 @@ func TestFirstOrCreateKeyword(t *testing.T) {
}
func TestKeyword_Updates(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
keyword := NewKeyword("KeywordBeforeUpdate")
assert.Equal(t, "keywordbeforeupdate", keyword.Keyword)
@@ -49,7 +49,7 @@ func TestKeyword_Updates(t *testing.T) {
}
func TestKeyword_Update(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
keyword := NewKeyword("KeywordBeforeUpdate2")
assert.Equal(t, "keywordbeforeupdate2", keyword.Keyword)
@@ -64,7 +64,7 @@ func TestKeyword_Update(t *testing.T) {
}
func TestKeyword_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
keyword := NewKeyword("KeywordName")
err := keyword.Save()

View File

@@ -1,6 +1,7 @@
package entity
import (
"fmt"
"sync"
"time"
@@ -50,6 +51,18 @@ func (Label) TableName() string {
return "labels"
}
// AfterUpdate flushes the label cache when a label is updated.
func (m *Label) AfterUpdate(tx *gorm.DB) (err error) {
FlushLabelCache()
return
}
// AfterDelete flushes the label cache when a label is deleted.
func (m *Label) AfterDelete(tx *gorm.DB) (err error) {
FlushLabelCache()
return
}
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Label) BeforeCreate(scope *gorm.Scope) error {
if rnd.IsUnique(m.LabelUID, LabelUID) {
@@ -101,6 +114,7 @@ func (m *Label) Create() error {
func (m *Label) Delete() error {
Db().Where("label_id = ? OR category_id = ?", m.ID, m.ID).Delete(&Category{})
Db().Where("label_id = ?", m.ID).Delete(&PhotoLabel{})
FlushLabelCache()
return Db().Delete(m).Error
}
@@ -152,17 +166,35 @@ func FirstOrCreateLabel(m *Label) *Label {
return nil
}
// FindLabel returns an existing row if exists.
func FindLabel(s string) *Label {
// FindLabel find the matching label based on the string provided or an error if not found.
func FindLabel(s string, cached bool) (m Label, err error) {
labelSlug := txt.Slug(s)
result := Label{}
if err := Db().Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).First(&result).Error; err == nil {
return &result
if labelSlug == "" {
return m, fmt.Errorf("invalid label slug %s", clean.LogQuote(labelSlug))
}
return nil
// Return cached label, if found.
if cached {
if cacheData, ok := labelCache.Get(labelSlug); ok {
log.Tracef("label: cache hit for %s", labelSlug)
return cacheData.(Label), nil
}
}
// Fetch and cache label from database.
m = Label{}
if r := Db().First(&m, "label_slug = ? OR custom_slug = ?", labelSlug, labelSlug); r.RecordNotFound() {
labelCache.Delete(labelSlug)
return m, fmt.Errorf("label not found")
} else if r.Error != nil {
labelCache.Delete(labelSlug)
return m, r.Error
} else {
labelCache.SetDefault(m.LabelSlug, m)
return m, nil
}
}
// AfterCreate sets the New column used for database callback

View File

@@ -0,0 +1,14 @@
package entity
import (
"time"
gc "github.com/patrickmn/go-cache"
)
var labelCache = gc.New(15*time.Minute, 15*time.Minute)
// FlushLabelCache resets the label cache.
func FlushLabelCache() {
labelCache.Flush()
}

View File

@@ -4,9 +4,9 @@ import (
"testing"
"time"
"github.com/photoprism/photoprism/internal/ai/classify"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/ai/classify"
)
func TestNewLabel(t *testing.T) {
@@ -24,6 +24,12 @@ func TestNewLabel(t *testing.T) {
})
}
func TestFlushLabelCache(t *testing.T) {
t.Run("Success", func(t *testing.T) {
FlushLabelCache()
})
}
func TestLabel_SetName(t *testing.T) {
t.Run("set name", func(t *testing.T) {
entity := LabelFixtures["landscape"]
@@ -135,7 +141,7 @@ func TestLabel_UpdateClassify(t *testing.T) {
}
func TestLabel_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
label := NewLabel("Unicorn2000", 5)
initialDate := label.UpdatedAt
err := label.Save()
@@ -151,7 +157,7 @@ func TestLabel_Save(t *testing.T) {
}
func TestLabel_Delete(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
label := NewLabel("LabelToBeDeleted", 5)
err := label.Save()
assert.False(t, label.Deleted())
@@ -178,7 +184,7 @@ func TestLabel_Delete(t *testing.T) {
}
func TestLabel_Restore(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
var deleteTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
label := &Label{DeletedAt: &deleteTime, LabelName: "ToBeRestored"}
@@ -205,24 +211,31 @@ func TestLabel_Restore(t *testing.T) {
}
func TestFindLabel(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("SaveAndFindWithCache", func(t *testing.T) {
label := &Label{LabelSlug: "find-me-label", LabelName: "Find Me"}
err := label.Save()
if err != nil {
t.Fatal(err)
saveErr := label.Save()
if saveErr != nil {
t.Fatal(saveErr)
}
r := FindLabel("find-me-label")
assert.Equal(t, "Find Me", r.LabelName)
uncached, findErr := FindLabel("find-me-label", false)
assert.NoError(t, findErr)
assert.Equal(t, "Find Me", uncached.LabelName)
cached, cacheErr := FindLabel("find-me-label", true)
assert.NoError(t, cacheErr)
assert.Equal(t, "Find Me", cached.LabelName)
assert.Equal(t, uncached.LabelSlug, cached.LabelSlug)
assert.Equal(t, uncached.ID, cached.ID)
assert.Equal(t, uncached.LabelUID, cached.LabelUID)
})
t.Run("nil", func(t *testing.T) {
r := FindLabel("XXX")
assert.Nil(t, r)
t.Run("NotFound", func(t *testing.T) {
r, err := FindLabel("XXX", true)
assert.Error(t, err)
assert.NotNil(t, r)
})
}
func TestLabel_Links(t *testing.T) {
t.Run("1 result", func(t *testing.T) {
t.Run("OneResult", func(t *testing.T) {
label := LabelFixtures.Get("flower")
links := label.Links()
assert.Equal(t, "6jxf3jfn2k", links[0].LinkToken)
@@ -230,7 +243,7 @@ func TestLabel_Links(t *testing.T) {
}
func TestLabel_Update(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
label := &Label{LabelSlug: "to-be-updated", LabelName: "Update Me Please"}
err := label.Save()
if err != nil {

View File

@@ -114,7 +114,7 @@ func TestLink_Save(t *testing.T) {
assert.Error(t, link.Save())
})
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
link := NewLink("ls6sg6bffgtredft", false, false)
err := link.Save()
@@ -126,7 +126,7 @@ func TestLink_Save(t *testing.T) {
}
func TestLink_Delete(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
link := NewLink("ls6sg6bffgtreoft", false, false)
err := link.Delete()
@@ -147,7 +147,7 @@ func TestLink_Delete(t *testing.T) {
}
func TestFindLink(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
m := NewLink("ls6sg6bffgtrjoft", false, false)
link := &m
@@ -194,14 +194,14 @@ func TestFindLinks(t *testing.T) {
}
func TestFindValidLinksLinks(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
r := FindValidLinks("1jxf3jfn2k", "")
assert.Equal(t, "as6sg6bxpogaaba8", r[0].ShareUID)
})
}
func TestLink_String(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
link := NewLink("jhgko", false, false)
uid := link.LinkUID
assert.Equal(t, uid, link.String())

View File

@@ -144,7 +144,7 @@ func TestMarker_SaveForm(t *testing.T) {
}
func TestUpdateOrCreateMarker(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "ls6sg6b1wowuy3c3", SrcImage, MarkerLabel, 100, 65)
assert.IsType(t, &Marker{}, m)
assert.Equal(t, "fs6sg6bw45bnlqdw", m.FileUID)
@@ -169,7 +169,7 @@ func TestUpdateOrCreateMarker(t *testing.T) {
}
func TestMarker_Updates(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "ls6sg6b1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
m, err := CreateMarkerIfNotExists(m)
@@ -194,7 +194,7 @@ func TestMarker_Updates(t *testing.T) {
}
func TestMarker_Update(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "ls6sg6b1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
m, err := CreateMarkerIfNotExists(m)
@@ -248,7 +248,7 @@ func TestMarker_InvalidArea(t *testing.T) {
// TODO fails on mariadb
func TestMarker_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "ls6sg6b1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
m, err := CreateMarkerIfNotExists(m)
@@ -428,7 +428,7 @@ func TestMarker_Create(t *testing.T) {
}
func TestMarker_Embeddings(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
m := MarkerFixtures.Get("1000003-4")
assert.Equal(t, 0.013083286379677253, m.Embeddings()[0][0])

View File

@@ -7,7 +7,7 @@ import (
)
func TestNewPassword(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "passwd", false)
assert.Len(t, p.Hash, 60)
})
@@ -102,7 +102,7 @@ func TestPassword_Invalid(t *testing.T) {
}
func TestPassword_Create(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
p := Password{}
err := p.Create()

View File

@@ -181,8 +181,8 @@ func SavePhotoForm(model Photo, form form.Photo) error {
details.Keywords = strings.Join(txt.UniqueWords(w), ", ")
}
if err := model.SyncKeywordLabels(); err != nil {
log.Errorf("photo: %s %s while syncing keywords and labels", model.String(), err)
if err := model.UpdateLabels(); err != nil {
log.Errorf("photo: %s %s while updating labels", model.String(), err)
}
if err := model.UpdateTitle(model.ClassifyLabels()); err != nil {
@@ -446,15 +446,97 @@ func (m *Photo) RemoveKeyword(w string) error {
return nil
}
// SyncKeywordLabels maintains the label / photo relationship for existing labels and keywords.
func (m *Photo) SyncKeywordLabels() error {
// UpdateLabels updates labels that are automatically set based on the photo title, subject, and keywords.
func (m *Photo) UpdateLabels() error {
if err := m.UpdateTitleLabels(); err != nil {
return err
}
if err := m.UpdateSubjectLabels(); err != nil {
return err
}
if err := m.UpdateKeywordLabels(); err != nil {
return err
}
return nil
}
// UpdateTitleLabels updates the labels assigned based on the photo title.
func (m *Photo) UpdateTitleLabels() error {
if m == nil {
return nil
} else if m.PhotoTitle == "" {
return nil
} else if SrcPriority[m.TitleSrc] < SrcPriority[SrcName] {
return nil
}
keywords := txt.UniqueKeywords(m.PhotoTitle)
var labelIds []uint
for _, w := range keywords {
if label, err := FindLabel(w, true); err == nil {
if label.Deleted() {
continue
}
labelIds = append(labelIds, label.ID)
FirstOrCreatePhotoLabel(NewPhotoLabel(m.ID, label.ID, 10, classify.SrcTitle))
}
}
return Db().Where("label_src = ? AND photo_id = ? AND label_id NOT IN (?)", classify.SrcTitle, m.ID, labelIds).Delete(&PhotoLabel{}).Error
}
// UpdateSubjectLabels updates the labels assigned based on photo subject metadata.
func (m *Photo) UpdateSubjectLabels() error {
details := m.GetDetails()
if details == nil {
return nil
} else if details.Subject == "" {
return nil
} else if SrcPriority[details.SubjectSrc] < SrcPriority[SrcName] {
return nil
}
keywords := txt.UniqueKeywords(details.Subject)
var labelIds []uint
for _, w := range keywords {
if label, err := FindLabel(w, true); err == nil {
if label.Deleted() {
continue
}
labelIds = append(labelIds, label.ID)
FirstOrCreatePhotoLabel(NewPhotoLabel(m.ID, label.ID, 15, classify.SrcSubject))
}
}
return Db().Where("label_src = ? AND photo_id = ? AND label_id NOT IN (?)", classify.SrcSubject, m.ID, labelIds).Delete(&PhotoLabel{}).Error
}
// UpdateKeywordLabels updates the labels assigned based on photo keyword metadata.
func (m *Photo) UpdateKeywordLabels() error {
details := m.GetDetails()
if details == nil {
return nil
} else if details.Keywords == "" {
return nil
}
keywords := txt.UniqueKeywords(details.Keywords)
var labelIds []uint
for _, w := range keywords {
if label := FindLabel(w); label != nil {
if label, err := FindLabel(w, true); err == nil {
if label.Deleted() {
continue
}

View File

@@ -59,7 +59,7 @@ func TestFirstOrCreatePhotoAlbum(t *testing.T) {
// TODO fails on mariadb
func TestPhotoAlbum_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
p := PhotoAlbum{}
err := p.Create()

View File

@@ -41,7 +41,7 @@ func TestFirstOrCreatePhotoLabel(t *testing.T) {
}
func TestPhotoLabel_ClassifyLabel(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
pl := LabelFixtures.PhotoLabel(1000000, "flower", 38, "image")
r := pl.ClassifyLabel()
assert.Equal(t, "Flower", r.Name)
@@ -57,7 +57,7 @@ func TestPhotoLabel_ClassifyLabel(t *testing.T) {
}
func TestPhotoLabel_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
photoLabel := NewPhotoLabel(13, 1000, 99, "image")
err := photoLabel.Save()
if err != nil {
@@ -78,7 +78,7 @@ func TestPhotoLabel_Save(t *testing.T) {
}
func TestPhotoLabel_Update(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
photoLabel := PhotoLabel{LabelID: 555, PhotoID: 888}
assert.Equal(t, uint(0x22b), photoLabel.LabelID)

View File

@@ -454,8 +454,8 @@ func (m *Photo) SaveLocation() error {
m.GetDetails().Keywords = strings.Join(txt.UniqueWords(w), ", ")
if err := m.SyncKeywordLabels(); err != nil {
log.Errorf("photo: %s %s while syncing keywords and labels", m.String(), err)
if err := m.UpdateKeywordLabels(); err != nil {
log.Errorf("photo: %s %s while updating keyword labels", m.String(), err)
}
if err := m.UpdateTitle(m.ClassifyLabels()); err != nil {

View File

@@ -35,7 +35,7 @@ func TestPhoto_Stackable(t *testing.T) {
}
func TestPhoto_IdenticalIdentical(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
photo := PhotoFixtures.Get("Photo19")
result, err := photo.Identical(true, true)
@@ -58,7 +58,7 @@ func TestPhoto_IdenticalIdentical(t *testing.T) {
}
assert.Equal(t, 0, len(result))
})
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
photo := PhotoFixtures.Get("Photo23")
result, err := photo.Identical(true, true)
@@ -70,7 +70,7 @@ func TestPhoto_IdenticalIdentical(t *testing.T) {
t.Logf("result: %#v", result)
assert.Equal(t, 2, len(result))
})
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
photo := PhotoFixtures.Get("Photo23")
result, err := photo.Identical(true, false)
@@ -84,7 +84,7 @@ func TestPhoto_IdenticalIdentical(t *testing.T) {
}
func TestPhoto_Merge(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
photo := PhotoFixtures.Get("Photo23")
original, merged, err := photo.Merge(true, false)

View File

@@ -430,24 +430,29 @@ func TestPhoto_RemoveKeyword(t *testing.T) {
})
}
func TestPhoto_SyncKeywordLabels(t *testing.T) {
t.Run("Ok", func(t *testing.T) {
labelotter := Label{LabelName: "otter", LabelSlug: "otter"}
func TestPhoto_UpdateLabels(t *testing.T) {
t.Run("Success", func(t *testing.T) {
labelNative := Label{LabelName: "Native", LabelSlug: "native"}
var deletedTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
labelsnake := Label{LabelName: "snake", LabelSlug: "snake", DeletedAt: &deletedTime}
labelWindow := Label{LabelName: "Window", LabelSlug: "window", DeletedAt: &deletedTime}
err := labelsnake.Save()
err := labelWindow.Save()
if err != nil {
t.Fatal(err)
}
err = labelotter.Save()
err = labelNative.Save()
if err != nil {
t.Fatal(err)
}
details := &Details{Keywords: "cow, flower, snake, otter"}
photo := Photo{ID: 34567, Details: details}
details := &Details{
Subject: "native",
SubjectSrc: SrcMeta,
Keywords: "cow, flower, snake, otter",
KeywordsSrc: SrcMeta,
}
photo := Photo{ID: 134567, PhotoTitle: "Cat in the House", Details: details}
err = photo.Save()
if err != nil {
@@ -458,7 +463,7 @@ func TestPhoto_SyncKeywordLabels(t *testing.T) {
assert.Equal(t, 0, len(p.Labels))
err = p.SyncKeywordLabels()
err = p.UpdateLabels()
if err != nil {
t.Fatal(err)
}
@@ -470,6 +475,133 @@ func TestPhoto_SyncKeywordLabels(t *testing.T) {
})
}
func TestPhoto_UpdateTitleLabels(t *testing.T) {
t.Run("Success", func(t *testing.T) {
labelFood := Label{LabelName: "Food", LabelSlug: "food"}
labelWine := Label{LabelName: "Wine", LabelSlug: "wine"}
labelBar := Label{LabelName: "Bar", LabelSlug: "bar", DeletedAt: TimeStamp()}
err := labelFood.Save()
if err != nil {
t.Fatal(err)
}
err = labelWine.Save()
if err != nil {
t.Fatal(err)
}
err = labelBar.Save()
if err != nil {
t.Fatal(err)
}
details := &Details{Keywords: "snake, otter, food", KeywordsSrc: SrcMeta}
photo := Photo{ID: 234567, PhotoTitle: "I was in a nice Wine Bar!", TitleSrc: SrcName, PhotoDescription: "cow, flower, food", DescriptionSrc: SrcMeta, Details: details}
err = photo.Save()
if err != nil {
t.Fatal(err)
}
p := FindPhoto(photo)
assert.Equal(t, 0, len(p.Labels))
err = p.UpdateTitleLabels()
if err != nil {
t.Fatal(err)
}
p = FindPhoto(*p)
assert.Equal(t, "I was in a nice Wine Bar!", p.PhotoTitle)
assert.Equal(t, "cow, flower, food", p.PhotoDescription)
assert.Equal(t, "snake, otter, food", p.Details.Keywords)
assert.Equal(t, 1, len(p.Labels))
})
}
func TestPhoto_UpdateSubjectLabels(t *testing.T) {
t.Run("Success", func(t *testing.T) {
labelEgg := Label{LabelName: "Egg", LabelSlug: "egg"}
var deletedTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
labelBird := Label{LabelName: "Bird", LabelSlug: "bird", DeletedAt: &deletedTime}
err := labelBird.Save()
if err != nil {
t.Fatal(err)
}
err = labelEgg.Save()
if err != nil {
t.Fatal(err)
}
details := &Details{Subject: "cow, egg, bird", SubjectSrc: SrcMeta}
photo := Photo{ID: 334567, TitleSrc: SrcName, Details: details}
err = photo.Save()
if err != nil {
t.Fatal(err)
}
p := FindPhoto(photo)
assert.Equal(t, 0, len(p.Labels))
err = p.UpdateSubjectLabels()
if err != nil {
t.Fatal(err)
}
p = FindPhoto(*p)
assert.Equal(t, "cow, egg, bird", p.Details.Subject)
assert.Equal(t, 2, len(p.Labels))
})
}
func TestPhoto_UpdateKeywordLabels(t *testing.T) {
t.Run("Success", func(t *testing.T) {
labelOtter := Label{LabelName: "Otter", LabelSlug: "otter"}
var deletedTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
labelSnake := Label{LabelName: "Snake", LabelSlug: "snake", DeletedAt: &deletedTime}
err := labelSnake.Save()
if err != nil {
t.Fatal(err)
}
err = labelOtter.Save()
if err != nil {
t.Fatal(err)
}
details := &Details{Keywords: "cow, flower, snake, otter", KeywordsSrc: SrcAuto}
photo := Photo{ID: 434567, Details: details}
err = photo.Save()
if err != nil {
t.Fatal(err)
}
p := FindPhoto(photo)
assert.Equal(t, 0, len(p.Labels))
err = p.UpdateKeywordLabels()
if err != nil {
t.Fatal(err)
}
p = FindPhoto(*p)
assert.Equal(t, "cow, flower, snake, otter", p.Details.Keywords)
assert.Equal(t, 3, len(p.Labels))
})
}
func TestPhoto_LocationLoaded(t *testing.T) {
t.Run("Photo", func(t *testing.T) {
photo := Photo{PhotoUID: "56798", PhotoName: "Holiday", OriginalName: "holidayOriginal2"}

View File

@@ -42,7 +42,7 @@ func TestPhoto_SetTitle(t *testing.T) {
m.SetTitle("NewTitleSet", SrcAuto)
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
})
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
m := PhotoFixtures.Get("Photo15")
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
m.SetTitle("NewTitleSet", SrcName)

View File

@@ -8,7 +8,7 @@ import (
// TODO find duplicates
func TestDuplicates(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
if files, err := Duplicates(10, 0, ""); err != nil {
t.Fatal(err)
} else if files == nil {

View File

@@ -7,7 +7,7 @@ import (
)
func TestSetDownloadFileID(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
err := SetDownloadFileID("exampleFileName.jpg", 1000000)
if err != nil {
t.Fatal(err)

View File

@@ -8,7 +8,7 @@ import (
)
func TestFileSyncs(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
r, err := FileSyncs(uint(1000001), "downloaded", 10)
if err != nil {
t.Fatal(err)

View File

@@ -194,7 +194,7 @@ func TestFileByHash(t *testing.T) {
}
func TestSetPhotoPrimary(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
assert.Equal(t, false, entity.FileFixturesExampleXMP.FilePrimary)
err := SetPhotoPrimary("ps6sg6be2lvl0yh7", "fs6sg6bwhhbnlqdn")
@@ -244,7 +244,7 @@ func TestRenameFile(t *testing.T) {
t.Fatal(err)
}
})
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
assert.Equal(t, "2790/02/Photo01.xmp", entity.FileFixturesExampleXMP.FileName)
assert.Equal(t, "/", entity.FileFixturesExampleXMP.FileRoot)
err := RenameFile("/", "exampleXmpFile.xmp", "test-root", "yyy.jpg")

View File

@@ -68,7 +68,7 @@ func TestAlbumFolders(t *testing.T) {
}
func TestUpdateFolderDates(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
if err := UpdateFolderDates(); err != nil {
t.Fatal(err)
}

View File

@@ -106,7 +106,7 @@ func TestOrphanPhotos(t *testing.T) {
// TODO How to verify?
func TestFixPrimaries(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
err := FixPrimaries()
if err != nil {
t.Fatal(err)

View File

@@ -5,7 +5,7 @@ import (
)
func TestCellIDs(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
result, err := CellIDs()
if err != nil {
@@ -16,7 +16,7 @@ func TestCellIDs(t *testing.T) {
})
}
func TestPurgePlaces(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
if err := PurgePlaces(); err != nil {
t.Fatal(err)
}

View File

@@ -7,7 +7,7 @@ import (
)
func TestRegisteredUsers(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
users := RegisteredUsers()
for _, user := range users {

View File

@@ -8,7 +8,7 @@ import (
)
func TestCreateService(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
account := Service{AccName: "Foo", AccOwner: "bar", AccURL: "test.com", AccType: "webdav", AccKey: "123", AccUser: "testuser", AccPass: "testpass",
AccError: "", AccShare: true, AccSync: true, RetryLimit: 4, SharePath: "/home", ShareSize: "500", ShareExpires: 3500, SyncPath: "/sync",
SyncInterval: 5, SyncUpload: true, SyncDownload: false, SyncFilenames: true, SyncRaw: false}
@@ -50,7 +50,7 @@ func TestCreateService(t *testing.T) {
}
func TestService_SaveForm(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
account := Service{AccName: "Foo", AccOwner: "bar", AccURL: "test.com", AccType: "test", AccKey: "123", AccUser: "testuser", AccPass: "testpass",
AccError: "", AccShare: true, AccSync: true, RetryLimit: 4, SharePath: "/home", ShareSize: "500", ShareExpires: 3500, SyncPath: "/sync",
SyncInterval: 5, SyncUpload: true, SyncDownload: true, SyncFilenames: true, SyncRaw: false}
@@ -94,7 +94,7 @@ func TestService_SaveForm(t *testing.T) {
}
func TestService_Delete(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
account := Service{AccName: "DeleteAccount", AccOwner: "Delete", AccURL: "test.com", AccType: "test", AccKey: "123", AccUser: "testuser", AccPass: "testpass",
AccError: "", AccShare: true, AccSync: true, RetryLimit: 4, SharePath: "/home", ShareSize: "500", ShareExpires: 3500, SyncPath: "/sync",
SyncInterval: 5, SyncUpload: true, SyncDownload: false, SyncFilenames: true, SyncRaw: false}
@@ -121,7 +121,7 @@ func TestService_Delete(t *testing.T) {
}
func TestService_Directories(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
account := Service{AccName: "DirectoriesAccount", AccOwner: "Owner", AccURL: "http://dummy-webdav/", AccType: "webdav", AccKey: "123", AccUser: "admin", AccPass: "photoprism",
AccError: "", AccShare: true, AccSync: true, RetryLimit: 4, SharePath: "/home", ShareSize: "500", ShareExpires: 3500, SyncPath: "/sync",
SyncInterval: 5, SyncUpload: true, SyncDownload: false, SyncFilenames: true, SyncRaw: false}
@@ -173,7 +173,7 @@ func TestService_Directories(t *testing.T) {
}
func TestService_Updates(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
account := Service{AccName: "DeleteAccount", AccOwner: "Delete", AccURL: "test.com", AccType: "test", AccKey: "123", AccUser: "testuser", AccPass: "testpass",
AccError: "", AccShare: true, AccSync: true, RetryLimit: 4, SharePath: "/home", ShareSize: "500", ShareExpires: 3500, SyncPath: "/sync",
SyncInterval: 5, SyncUpload: true, SyncDownload: false, SyncFilenames: true, SyncRaw: false}
@@ -203,7 +203,7 @@ func TestService_Updates(t *testing.T) {
}
func TestService_Update(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
account := Service{AccName: "DeleteAccount", AccOwner: "Delete", AccURL: "test.com", AccType: "test", AccKey: "123", AccUser: "testuser", AccPass: "testpass",
AccError: "", AccShare: true, AccSync: true, RetryLimit: 4, SharePath: "/home", ShareSize: "500", ShareExpires: 3500, SyncPath: "/sync",
SyncInterval: 5, SyncUpload: true, SyncDownload: false, SyncFilenames: true, SyncRaw: false}
@@ -231,7 +231,7 @@ func TestService_Update(t *testing.T) {
// TODO fails on mariadb
func TestService_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
account := Service{AccName: "DeleteAccount", AccOwner: "Delete", AccURL: "test.com", AccType: "test", AccKey: "123", AccUser: "testuser", AccPass: "testpass",
AccError: "", AccShare: true, AccSync: true, RetryLimit: 4, SharePath: "/home", ShareSize: "500", ShareExpires: 3500, SyncPath: "/sync",
SyncInterval: 5, SyncUpload: true, SyncDownload: false, SyncFilenames: true, SyncRaw: false}
@@ -259,7 +259,7 @@ func TestService_Save(t *testing.T) {
}
func TestService_Create(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
account := Service{}
err := account.Create()

View File

@@ -16,6 +16,8 @@ const (
SrcLocation = classify.SrcLocation // Prio 8
SrcMarker = "marker" // Prio 8
SrcImage = classify.SrcImage // Prio 8
SrcTitle = classify.SrcTitle // Prio 16
SrcSubject = classify.SrcSubject // Prio 16
SrcKeyword = classify.SrcKeyword // Prio 16
SrcMeta = "meta" // Prio 16
SrcXmp = "xmp" // Prio 32
@@ -44,6 +46,8 @@ var SrcPriority = Priorities{
SrcLocation: 8,
SrcMarker: 8,
SrcImage: 8,
SrcTitle: 16,
SrcSubject: 16,
SrcKeyword: 16,
SrcMeta: 16,
SrcXmp: 32,

View File

@@ -911,8 +911,8 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
return result
}
if err = photo.SyncKeywordLabels(); err != nil {
log.Errorf("index: %s in %s (sync keywords and labels)", err, logName)
if err = photo.UpdateLabels(); err != nil {
log.Errorf("index: %s in %s (update labels)", err, logName)
}
if err = photo.IndexKeywords(); err != nil {