Files
photoprism/internal/entity/label.go
2025-02-04 03:35:01 +01:00

352 lines
8.5 KiB
Go

package entity
import (
"fmt"
"sync"
"time"
"github.com/jinzhu/gorm"
"github.com/ulule/deepcopier"
"github.com/photoprism/photoprism/internal/ai/classify"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
)
const (
LabelUID = byte('l')
)
var labelMutex = sync.Mutex{}
var labelCategoriesMutex = sync.Mutex{}
type Labels []Label
// Label is used for photo, album and location categorization
type Label struct {
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
LabelUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
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"`
LabelPriority int `json:"Priority" yaml:"Priority,omitempty"`
LabelFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
LabelDescription string `gorm:"type:VARCHAR(2048);" json:"Description" yaml:"Description,omitempty"`
LabelNotes string `gorm:"type:VARCHAR(1024);" json:"Notes" yaml:"Notes,omitempty"`
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id" json:"-" yaml:"-"`
PhotoCount int `gorm:"default:1" json:"PhotoCount" yaml:"-"`
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"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
PublishedAt *time.Time `sql:"index" json:"PublishedAt,omitempty" yaml:"PublishedAt,omitempty"`
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
New bool `gorm:"-" json:"-" yaml:"-"`
}
// TableName returns the entity table name.
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
}
// AfterCreate sets the New column used for database callback
func (m *Label) AfterCreate(scope *gorm.Scope) error {
m.New = true
FlushLabelCache()
return nil
}
// 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) {
return nil
}
return scope.SetColumn("LabelUID", rnd.GenerateUID(LabelUID))
}
// NewLabel returns a new label.
func NewLabel(name string, priority int) *Label {
labelName := txt.Clip(name, txt.ClipDefault)
if labelName == "" {
labelName = "Unknown"
}
labelName = txt.Title(labelName)
labelSlug := txt.Slug(labelName)
result := &Label{
LabelSlug: labelSlug,
CustomSlug: labelSlug,
LabelName: txt.Clip(labelName, txt.ClipName),
LabelPriority: priority,
PhotoCount: 1,
}
return result
}
// Save updates the record in the database or inserts a new record if it does not already exist.
func (m *Label) Save() error {
labelMutex.Lock()
defer labelMutex.Unlock()
return Db().Save(m).Error
}
// SaveForm updates the entity using form data and stores it in the database.
func (m *Label) SaveForm(f *form.Label) error {
if f == nil {
return fmt.Errorf("form is nil")
} else if f.LabelName == "" || txt.Slug(f.LabelName) == "" {
return ErrInvalidName
}
labelMutex.Lock()
defer labelMutex.Unlock()
if err := deepcopier.Copy(m).From(f); err != nil {
return err
}
if m.SetName(f.LabelName) {
return Db().Save(m).Error
} else {
return ErrInvalidName
}
}
// Create inserts the label to the database.
func (m *Label) Create() error {
labelMutex.Lock()
defer labelMutex.Unlock()
return UnscopedDb().Create(m).Error
}
// 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{})
FlushLabelCache()
return Db().Delete(m).Error
}
// Deleted returns true if the label is deleted.
func (m *Label) Deleted() bool {
if m.DeletedAt == nil {
return false
}
return !m.DeletedAt.IsZero()
}
// Restore restores the label in the database.
func (m *Label) Restore() error {
if m.Deleted() {
return UnscopedDb().Model(m).Update("DeletedAt", nil).Error
}
return nil
}
// HasID tests if the entity has an ID and a valid UID.
func (m *Label) HasID() bool {
if m == nil {
return false
}
return m.ID > 0 && m.HasUID()
}
// HasUID tests if the entity has a valid UID.
func (m *Label) HasUID() bool {
if m == nil {
return false
}
return rnd.IsUID(m.LabelUID, LabelUID)
}
// Skip tests if the entity has invalid IDs or has been deleted and therefore should not be assigned.
func (m *Label) Skip() bool {
if m == nil {
return true
} else if !m.HasID() {
return true
} else if m.Deleted() {
return true
}
return false
}
// Update a label property in the database.
func (m *Label) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
}
// FirstOrCreateLabel returns the existing label, inserts a new label or nil in case of errors.
func FirstOrCreateLabel(m *Label) *Label {
if m.LabelSlug == "" && m.CustomSlug == "" {
return nil
}
result := &Label{}
if err := UnscopedDb().
Where("(custom_slug <> '' AND custom_slug = ? OR label_slug <> '' AND label_slug = ?)", m.CustomSlug, m.LabelSlug).
First(result).Error; err == nil {
return result
} else if createErr := m.Create(); createErr == nil {
if m.LabelPriority >= 0 {
event.EntitiesCreated("labels", []*Label{m})
event.Publish("count.labels", event.Data{
"count": 1,
})
}
return m
} else if err = UnscopedDb().
Where("(custom_slug <> '' AND custom_slug = ? OR label_slug <> '' AND label_slug = ?)", m.CustomSlug, m.LabelSlug).
First(result).Error; err == nil {
return result
} else {
log.Errorf("label: %s (find or create %s)", createErr, m.LabelSlug)
}
return nil
}
// SetName changes the label name.
func (m *Label) SetName(name string) bool {
labelName := txt.Clip(clean.NameCapitalized(name), txt.ClipName)
if labelName == "" {
return false
}
labelSlug := txt.Slug(labelName)
if labelSlug == "" {
return false
}
m.LabelName = labelName
m.CustomSlug = labelSlug
if m.LabelSlug == "" {
m.LabelSlug = labelSlug
}
return true
}
// InvalidName checks if the label name is invalid.
func (m *Label) InvalidName() bool {
labelName := txt.Clip(clean.NameCapitalized(m.LabelName), txt.ClipName)
if labelName == "" {
return true
}
labelSlug := txt.Slug(labelName)
if labelSlug == "" {
return true
}
return false
}
// GetSlug returns the label slug.
func (m *Label) GetSlug() string {
if m.CustomSlug != "" {
return m.CustomSlug
} else if m.LabelSlug != "" {
return m.LabelSlug
}
return txt.Slug(m.LabelName)
}
// UpdateClassify updates a label if necessary
func (m *Label) UpdateClassify(label classify.Label) error {
save := false
db := Db()
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 {
if m.SetName(label.Title()) {
save = true
} else {
return ErrInvalidName
}
}
// Save label.
if save {
if err := db.Save(m).Error; err != nil {
return err
}
}
// Update label categories.
if len(label.Categories) > 0 {
labelCategoriesMutex.Lock()
defer labelCategoriesMutex.Unlock()
for _, category := range label.Categories {
sn := FirstOrCreateLabel(NewLabel(txt.Title(category), -3))
if sn == nil {
continue
}
if sn.Skip() {
continue
}
if err := db.Model(m).Association("LabelCategories").Append(sn).Error; err != nil {
log.Debugf("index: failed saving label category %s (%s)", clean.Log(category), err)
}
}
}
return nil
}
// Links returns all share links for this entity.
func (m *Label) Links() Links {
return FindLinks("", m.LabelUID)
}