2019-12-11 16:55:18 +01:00
|
|
|
package entity
|
2019-06-04 18:26:35 +02:00
|
|
|
|
|
|
|
import (
|
2020-12-13 15:43:01 +01:00
|
|
|
"sync"
|
2019-12-27 05:18:52 +01:00
|
|
|
"time"
|
2019-06-04 18:26:35 +02:00
|
|
|
|
|
|
|
"github.com/jinzhu/gorm"
|
2021-12-15 12:24:05 +01:00
|
|
|
|
2020-04-17 21:20:38 +02:00
|
|
|
"github.com/photoprism/photoprism/internal/classify"
|
2020-05-26 11:00:39 +02:00
|
|
|
"github.com/photoprism/photoprism/internal/event"
|
2021-12-15 12:24:05 +01:00
|
|
|
|
2022-04-15 09:42:07 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
2020-01-12 14:00:56 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/rnd"
|
2020-02-02 02:00:47 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
2019-06-04 18:26:35 +02:00
|
|
|
)
|
|
|
|
|
2020-12-13 15:43:01 +01:00
|
|
|
var labelMutex = sync.Mutex{}
|
2022-04-03 12:43:21 +02:00
|
|
|
var labelCategoriesMutex = sync.Mutex{}
|
2020-12-13 15:43:01 +01:00
|
|
|
|
2020-06-02 17:57:12 +02:00
|
|
|
type Labels []Label
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// Label is used for photo, album and location categorization
|
2019-06-04 18:26:35 +02:00
|
|
|
type Label struct {
|
2020-05-23 20:58:58 +02:00
|
|
|
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
|
2020-12-15 20:14:06 +01:00
|
|
|
LabelUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
|
2021-09-23 23:46:17 +02:00
|
|
|
LabelSlug string `gorm:"type:VARBINARY(160);unique_index;" json:"Slug" yaml:"-"`
|
|
|
|
CustomSlug string `gorm:"type:VARBINARY(160);index;" json:"CustomSlug" yaml:"-"`
|
|
|
|
LabelName string `gorm:"type:VARCHAR(160);" json:"Name" yaml:"Name"`
|
2020-12-15 20:14:06 +01:00
|
|
|
LabelPriority int `json:"Priority" yaml:"Priority,omitempty"`
|
|
|
|
LabelFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
|
2022-03-30 20:36:25 +02:00
|
|
|
LabelDescription string `gorm:"type:VARCHAR(2048);" json:"Description" yaml:"Description,omitempty"`
|
|
|
|
LabelNotes string `gorm:"type:VARCHAR(1024);" json:"Notes" yaml:"Notes,omitempty"`
|
2020-05-23 20:58:58 +02:00
|
|
|
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id" json:"-" yaml:"-"`
|
|
|
|
PhotoCount int `gorm:"default:1" json:"PhotoCount" yaml:"-"`
|
2021-09-20 16:17:10 +02:00
|
|
|
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb" yaml:"Thumb,omitempty"`
|
|
|
|
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
|
2020-05-23 20:58:58 +02:00
|
|
|
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
|
|
|
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
|
|
|
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
|
|
|
|
New bool `gorm:"-" json:"-" yaml:"-"`
|
2019-06-04 18:26:35 +02:00
|
|
|
}
|
|
|
|
|
2021-08-30 18:58:27 +02:00
|
|
|
// TableName returns the entity database table name.
|
|
|
|
func (Label) TableName() string {
|
|
|
|
return "labels"
|
|
|
|
}
|
|
|
|
|
2020-05-23 20:58:58 +02:00
|
|
|
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
2019-12-16 23:33:52 +01:00
|
|
|
func (m *Label) BeforeCreate(scope *gorm.Scope) error {
|
2022-04-15 09:42:07 +02:00
|
|
|
if rnd.ValidID(m.LabelUID, 'l') {
|
2020-05-01 12:57:26 +02:00
|
|
|
return nil
|
2019-12-16 23:33:52 +01:00
|
|
|
}
|
|
|
|
|
2022-04-15 09:42:07 +02:00
|
|
|
return scope.SetColumn("LabelUID", rnd.GenerateUID('l'))
|
2019-12-16 23:33:52 +01:00
|
|
|
}
|
|
|
|
|
2020-06-02 17:57:12 +02:00
|
|
|
// NewLabel returns a new label.
|
2020-04-26 14:31:33 +02:00
|
|
|
func NewLabel(name string, priority int) *Label {
|
|
|
|
labelName := txt.Clip(name, txt.ClipDefault)
|
2019-06-04 18:26:35 +02:00
|
|
|
|
|
|
|
if labelName == "" {
|
|
|
|
labelName = "Unknown"
|
|
|
|
}
|
|
|
|
|
2020-04-26 14:31:33 +02:00
|
|
|
labelName = txt.Title(labelName)
|
2021-09-23 23:46:17 +02:00
|
|
|
labelSlug := txt.Slug(labelName)
|
2019-06-04 18:26:35 +02:00
|
|
|
|
|
|
|
result := &Label{
|
2019-06-09 04:37:02 +02:00
|
|
|
LabelSlug: labelSlug,
|
2020-04-17 21:20:38 +02:00
|
|
|
CustomSlug: labelSlug,
|
2021-09-23 23:46:17 +02:00
|
|
|
LabelName: txt.Clip(labelName, txt.ClipName),
|
2020-04-26 14:31:33 +02:00
|
|
|
LabelPriority: priority,
|
2020-05-10 19:43:49 +02:00
|
|
|
PhotoCount: 1,
|
2019-06-04 18:26:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2020-06-02 17:57:12 +02:00
|
|
|
// Save updates the existing or inserts a new label.
|
2020-05-26 11:00:39 +02:00
|
|
|
func (m *Label) Save() error {
|
2020-12-13 15:43:01 +01:00
|
|
|
labelMutex.Lock()
|
|
|
|
defer labelMutex.Unlock()
|
|
|
|
|
2020-05-26 11:00:39 +02:00
|
|
|
return Db().Save(m).Error
|
|
|
|
}
|
|
|
|
|
2020-06-02 17:57:12 +02:00
|
|
|
// Create inserts the label to the database.
|
2020-05-26 11:00:39 +02:00
|
|
|
func (m *Label) Create() error {
|
2020-12-13 15:43:01 +01:00
|
|
|
labelMutex.Lock()
|
|
|
|
defer labelMutex.Unlock()
|
|
|
|
|
2020-05-26 11:00:39 +02:00
|
|
|
return Db().Create(m).Error
|
|
|
|
}
|
|
|
|
|
2020-06-02 17:57:12 +02:00
|
|
|
// Delete removes the label from the database.
|
|
|
|
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{})
|
|
|
|
return Db().Delete(m).Error
|
|
|
|
}
|
|
|
|
|
|
|
|
// Deleted returns true if the label is deleted.
|
|
|
|
func (m *Label) Deleted() bool {
|
2022-04-21 22:17:26 +02:00
|
|
|
if m.DeletedAt == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return !m.DeletedAt.IsZero()
|
2020-06-02 17:57:12 +02:00
|
|
|
}
|
|
|
|
|
2021-08-11 21:42:31 +02:00
|
|
|
// Restore restores the label in the database.
|
2020-06-02 17:57:12 +02:00
|
|
|
func (m *Label) Restore() error {
|
|
|
|
if m.Deleted() {
|
|
|
|
return UnscopedDb().Model(m).Update("DeletedAt", nil).Error
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-08-11 21:42:31 +02:00
|
|
|
// Update a label property in the database.
|
2020-05-30 14:52:47 +02:00
|
|
|
func (m *Label) Update(attr string, value interface{}) error {
|
2022-04-04 08:54:03 +02:00
|
|
|
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
|
2020-05-30 14:52:47 +02:00
|
|
|
}
|
|
|
|
|
2020-06-02 17:57:12 +02:00
|
|
|
// FirstOrCreateLabel returns the existing label, inserts a new label or nil in case of errors.
|
2020-05-26 11:00:39 +02:00
|
|
|
func FirstOrCreateLabel(m *Label) *Label {
|
|
|
|
result := Label{}
|
|
|
|
|
2020-06-02 17:57:12 +02:00
|
|
|
if err := UnscopedDb().Where("label_slug = ? OR custom_slug = ?", m.LabelSlug, m.CustomSlug).First(&result).Error; err == nil {
|
2020-05-26 11:00:39 +02:00
|
|
|
return &result
|
2020-07-07 20:44:33 +02:00
|
|
|
} else if createErr := m.Create(); createErr == nil {
|
|
|
|
if m.LabelPriority >= 0 {
|
|
|
|
event.EntitiesCreated("labels", []*Label{m})
|
2020-05-26 11:00:39 +02:00
|
|
|
|
2020-07-07 20:44:33 +02:00
|
|
|
event.Publish("count.labels", event.Data{
|
|
|
|
"count": 1,
|
|
|
|
})
|
|
|
|
}
|
2020-05-26 11:00:39 +02:00
|
|
|
|
2020-07-07 20:44:33 +02:00
|
|
|
return m
|
|
|
|
} else if err := UnscopedDb().Where("label_slug = ? OR custom_slug = ?", m.LabelSlug, m.CustomSlug).First(&result).Error; err == nil {
|
|
|
|
return &result
|
|
|
|
} else {
|
2020-12-14 13:31:18 +01:00
|
|
|
log.Errorf("label: %s (find or create %s)", createErr, m.LabelSlug)
|
2019-12-19 09:37:10 +01:00
|
|
|
}
|
2019-06-04 18:26:35 +02:00
|
|
|
|
2020-07-07 20:44:33 +02:00
|
|
|
return nil
|
2019-06-04 18:26:35 +02:00
|
|
|
}
|
2019-12-11 04:12:54 +01:00
|
|
|
|
2020-05-28 21:20:42 +02:00
|
|
|
// FindLabel returns an existing row if exists.
|
|
|
|
func FindLabel(s string) *Label {
|
2021-09-23 23:46:17 +02:00
|
|
|
labelSlug := txt.Slug(s)
|
2020-05-28 21:20:42 +02:00
|
|
|
|
|
|
|
result := Label{}
|
|
|
|
|
|
|
|
if err := Db().Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).First(&result).Error; err == nil {
|
|
|
|
return &result
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// AfterCreate sets the New column used for database callback
|
2019-12-11 04:12:54 +01:00
|
|
|
func (m *Label) AfterCreate(scope *gorm.Scope) error {
|
2020-05-19 12:30:26 +02:00
|
|
|
m.New = true
|
|
|
|
return nil
|
2019-12-11 04:12:54 +01:00
|
|
|
}
|
2020-02-02 02:00:47 +01:00
|
|
|
|
2020-04-26 14:31:33 +02:00
|
|
|
// SetName changes the label name.
|
|
|
|
func (m *Label) SetName(name string) {
|
2022-04-15 09:42:07 +02:00
|
|
|
name = clean.Name(name)
|
2020-02-02 02:00:47 +01:00
|
|
|
|
2021-09-19 13:35:44 +02:00
|
|
|
if name == "" {
|
2020-04-17 21:20:38 +02:00
|
|
|
return
|
2020-02-02 02:00:47 +01:00
|
|
|
}
|
|
|
|
|
2021-09-23 23:46:17 +02:00
|
|
|
m.LabelName = txt.Clip(name, txt.ClipName)
|
|
|
|
m.CustomSlug = txt.Slug(name)
|
2020-04-17 21:20:38 +02:00
|
|
|
}
|
|
|
|
|
2021-08-11 21:42:31 +02:00
|
|
|
// UpdateClassify updates a label if necessary
|
2020-05-26 11:00:39 +02:00
|
|
|
func (m *Label) UpdateClassify(label classify.Label) error {
|
2020-04-17 21:20:38 +02:00
|
|
|
save := false
|
2020-04-30 20:07:03 +02:00
|
|
|
db := Db()
|
2020-04-17 21:20:38 +02:00
|
|
|
|
|
|
|
if m.LabelPriority != label.Priority {
|
|
|
|
m.LabelPriority = label.Priority
|
|
|
|
save = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if m.CustomSlug == "" {
|
|
|
|
m.CustomSlug = m.LabelSlug
|
|
|
|
save = true
|
|
|
|
} else if m.LabelSlug == "" {
|
|
|
|
m.LabelSlug = m.CustomSlug
|
|
|
|
save = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if m.CustomSlug == m.LabelSlug && label.Title() != m.LabelName {
|
2020-04-26 14:31:33 +02:00
|
|
|
m.SetName(label.Title())
|
2020-04-17 21:20:38 +02:00
|
|
|
save = true
|
|
|
|
}
|
|
|
|
|
2022-04-03 12:43:21 +02:00
|
|
|
// Save label.
|
2022-04-04 00:57:13 +02:00
|
|
|
if save {
|
|
|
|
if err := db.Save(m).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-04-03 12:43:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update label categories.
|
2022-04-04 00:57:13 +02:00
|
|
|
if len(label.Categories) > 0 {
|
|
|
|
labelCategoriesMutex.Lock()
|
|
|
|
defer labelCategoriesMutex.Unlock()
|
2022-04-03 12:43:21 +02:00
|
|
|
|
2022-04-04 00:57:13 +02:00
|
|
|
for _, category := range label.Categories {
|
|
|
|
sn := FirstOrCreateLabel(NewLabel(txt.Title(category), -3))
|
2020-05-26 11:00:39 +02:00
|
|
|
|
2022-04-04 00:57:13 +02:00
|
|
|
if sn == nil {
|
|
|
|
continue
|
|
|
|
}
|
2020-05-26 11:00:39 +02:00
|
|
|
|
2022-04-04 00:57:13 +02:00
|
|
|
if sn.Deleted() {
|
|
|
|
continue
|
|
|
|
}
|
2020-06-02 17:57:12 +02:00
|
|
|
|
2022-04-04 00:57:13 +02:00
|
|
|
if err := db.Model(m).Association("LabelCategories").Append(sn).Error; err != nil {
|
2022-04-15 09:42:07 +02:00
|
|
|
log.Debugf("index: failed saving label category %s (%s)", clean.Log(category), err)
|
2022-04-04 00:57:13 +02:00
|
|
|
}
|
2020-04-18 23:20:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2020-02-02 02:00:47 +01:00
|
|
|
}
|
2020-06-22 15:16:26 +02:00
|
|
|
|
|
|
|
// Links returns all share links for this entity.
|
|
|
|
func (m *Label) Links() Links {
|
|
|
|
return FindLinks("", m.LabelUID)
|
|
|
|
}
|