2020-05-18 22:18:58 +02:00
|
|
|
package entity
|
|
|
|
|
2020-07-07 10:51:55 +02:00
|
|
|
import (
|
|
|
|
"fmt"
|
2020-12-14 13:31:18 +01:00
|
|
|
"sync"
|
2020-07-07 10:51:55 +02:00
|
|
|
"time"
|
2020-12-31 13:51:31 +01:00
|
|
|
|
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
2020-07-07 10:51:55 +02:00
|
|
|
)
|
2020-07-06 19:15:57 +02:00
|
|
|
|
2020-12-14 13:31:18 +01:00
|
|
|
var photoDetailsMutex = sync.Mutex{}
|
|
|
|
|
2020-05-18 22:18:58 +02:00
|
|
|
// Details stores additional metadata fields for each photo to improve search performance.
|
|
|
|
type Details struct {
|
2020-12-31 13:51:31 +01:00
|
|
|
PhotoID uint `gorm:"primary_key;auto_increment:false" yaml:"-"`
|
|
|
|
Keywords string `gorm:"type:TEXT;" json:"Keywords" yaml:"Keywords"`
|
|
|
|
KeywordsSrc string `gorm:"type:VARBINARY(8);" json:"KeywordsSrc" yaml:"KeywordsSrc,omitempty"`
|
|
|
|
Notes string `gorm:"type:TEXT;" json:"Notes" yaml:"Notes,omitempty"`
|
|
|
|
NotesSrc string `gorm:"type:VARBINARY(8);" json:"NotesSrc" yaml:"NotesSrc,omitempty"`
|
|
|
|
Subject string `gorm:"type:VARCHAR(255);" json:"Subject" yaml:"Subject,omitempty"`
|
|
|
|
SubjectSrc string `gorm:"type:VARBINARY(8);" json:"SubjectSrc" yaml:"SubjectSrc,omitempty"`
|
|
|
|
Artist string `gorm:"type:VARCHAR(255);" json:"Artist" yaml:"Artist,omitempty"`
|
|
|
|
ArtistSrc string `gorm:"type:VARBINARY(8);" json:"ArtistSrc" yaml:"ArtistSrc,omitempty"`
|
|
|
|
Copyright string `gorm:"type:VARCHAR(255);" json:"Copyright" yaml:"Copyright,omitempty"`
|
|
|
|
CopyrightSrc string `gorm:"type:VARBINARY(8);" json:"CopyrightSrc" yaml:"CopyrightSrc,omitempty"`
|
|
|
|
License string `gorm:"type:VARCHAR(255);" json:"License" yaml:"License,omitempty"`
|
|
|
|
LicenseSrc string `gorm:"type:VARBINARY(8);" json:"LicenseSrc" yaml:"LicenseSrc,omitempty"`
|
|
|
|
CreatedAt time.Time `yaml:"-"`
|
|
|
|
UpdatedAt time.Time `yaml:"-"`
|
2020-07-06 19:15:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewDetails creates new photo details.
|
|
|
|
func NewDetails(photo Photo) Details {
|
|
|
|
return Details{PhotoID: photo.ID}
|
2020-05-18 22:18:58 +02:00
|
|
|
}
|
|
|
|
|
2020-05-26 11:00:39 +02:00
|
|
|
// Create inserts a new row to the database.
|
|
|
|
func (m *Details) Create() error {
|
2020-12-14 13:31:18 +01:00
|
|
|
photoDetailsMutex.Lock()
|
|
|
|
defer photoDetailsMutex.Unlock()
|
|
|
|
|
2020-07-07 10:51:55 +02:00
|
|
|
if m.PhotoID == 0 {
|
|
|
|
return fmt.Errorf("details: photo id must not be empty (create)")
|
|
|
|
}
|
|
|
|
|
2020-07-07 12:59:47 +02:00
|
|
|
return UnscopedDb().Create(m).Error
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save updates existing photo details or inserts a new row.
|
|
|
|
func (m *Details) Save() error {
|
|
|
|
if m.PhotoID == 0 {
|
|
|
|
return fmt.Errorf("details: photo id must not be empty (save)")
|
|
|
|
}
|
|
|
|
|
|
|
|
return UnscopedDb().Save(m).Error
|
2020-05-26 11:00:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// FirstOrCreateDetails returns the existing row, inserts a new row or nil in case of errors.
|
|
|
|
func FirstOrCreateDetails(m *Details) *Details {
|
|
|
|
result := Details{}
|
|
|
|
|
2020-07-09 17:45:56 +02:00
|
|
|
if err := m.Create(); err == nil {
|
|
|
|
return m
|
|
|
|
} else if err := Db().Where("photo_id = ?", m.PhotoID).First(&result).Error; err == nil {
|
2020-07-06 19:15:57 +02:00
|
|
|
if m.CreatedAt.IsZero() {
|
|
|
|
m.CreatedAt = Timestamp()
|
|
|
|
}
|
|
|
|
|
2020-05-26 11:00:39 +02:00
|
|
|
return &result
|
2020-07-09 17:45:56 +02:00
|
|
|
} else {
|
2020-12-14 13:31:18 +01:00
|
|
|
log.Errorf("details: %s (find or create %d)", err, m.PhotoID)
|
2020-05-26 11:00:39 +02:00
|
|
|
}
|
|
|
|
|
2020-07-09 17:45:56 +02:00
|
|
|
return nil
|
2020-05-18 22:18:58 +02:00
|
|
|
}
|
|
|
|
|
2020-12-31 13:51:31 +01:00
|
|
|
// NoKeywords tests if the photo has no Keywords.
|
2020-05-18 22:18:58 +02:00
|
|
|
func (m *Details) NoKeywords() bool {
|
|
|
|
return m.Keywords == ""
|
|
|
|
}
|
|
|
|
|
2020-12-31 13:51:31 +01:00
|
|
|
// NoSubject tests if the photo has no Subject.
|
2020-05-18 22:18:58 +02:00
|
|
|
func (m *Details) NoSubject() bool {
|
|
|
|
return m.Subject == ""
|
|
|
|
}
|
|
|
|
|
2020-12-31 13:51:31 +01:00
|
|
|
// NoNotes tests if the photo has no Notes.
|
2020-05-18 22:18:58 +02:00
|
|
|
func (m *Details) NoNotes() bool {
|
|
|
|
return m.Notes == ""
|
|
|
|
}
|
|
|
|
|
2020-12-31 13:51:31 +01:00
|
|
|
// NoArtist tests if the photo has no Artist.
|
2020-05-18 22:18:58 +02:00
|
|
|
func (m *Details) NoArtist() bool {
|
|
|
|
return m.Artist == ""
|
|
|
|
}
|
|
|
|
|
2020-12-31 13:51:31 +01:00
|
|
|
// NoCopyright tests if the photo has no Copyright.
|
2020-05-18 22:18:58 +02:00
|
|
|
func (m *Details) NoCopyright() bool {
|
|
|
|
return m.Copyright == ""
|
|
|
|
}
|
2020-12-31 13:51:31 +01:00
|
|
|
|
|
|
|
// NoLicense tests if the photo has no License.
|
|
|
|
func (m *Details) NoLicense() bool {
|
|
|
|
return m.License == ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// HasKeywords tests if the photo has a Keywords.
|
|
|
|
func (m *Details) HasKeywords() bool {
|
|
|
|
return !m.NoKeywords()
|
|
|
|
}
|
|
|
|
|
|
|
|
// HasSubject tests if the photo has a Subject.
|
|
|
|
func (m *Details) HasSubject() bool {
|
|
|
|
return !m.NoSubject()
|
|
|
|
}
|
|
|
|
|
|
|
|
// HasNotes tests if the photo has a Notes.
|
|
|
|
func (m *Details) HasNotes() bool {
|
|
|
|
return !m.NoNotes()
|
|
|
|
}
|
|
|
|
|
|
|
|
// HasArtist tests if the photo has an Artist.
|
|
|
|
func (m *Details) HasArtist() bool {
|
|
|
|
return !m.NoArtist()
|
|
|
|
}
|
|
|
|
|
|
|
|
// HasCopyright tests if the photo has a Copyright
|
|
|
|
func (m *Details) HasCopyright() bool {
|
|
|
|
return !m.NoCopyright()
|
|
|
|
}
|
|
|
|
|
|
|
|
// HasLicense tests if the photo has a License.
|
|
|
|
func (m *Details) HasLicense() bool {
|
|
|
|
return !m.NoLicense()
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetKeywords updates the photo details field.
|
|
|
|
func (m *Details) SetKeywords(data, src string) {
|
|
|
|
val := txt.Clip(data, txt.ClipDescription)
|
|
|
|
|
|
|
|
if val == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SrcPriority[src] < SrcPriority[m.KeywordsSrc]) && m.HasKeywords() {
|
2021-05-04 15:02:54 +02:00
|
|
|
// Ignore if priority is lower and keywords already exist.
|
2020-12-31 13:51:31 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-04 15:02:54 +02:00
|
|
|
if SrcPriority[src] > SrcPriority[m.KeywordsSrc] {
|
|
|
|
// Overwrite existing keywords if priority is higher.
|
|
|
|
m.Keywords = val
|
|
|
|
} else {
|
|
|
|
// Merge keywords if priority is the same.
|
|
|
|
m.Keywords = txt.MergeWords(m.Keywords, val)
|
|
|
|
}
|
|
|
|
|
2020-12-31 13:51:31 +01:00
|
|
|
m.KeywordsSrc = src
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetSubject updates the photo details field.
|
|
|
|
func (m *Details) SetSubject(data, src string) {
|
|
|
|
val := txt.Clip(data, txt.ClipVarchar)
|
|
|
|
|
|
|
|
if val == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SrcPriority[src] < SrcPriority[m.SubjectSrc]) && m.HasSubject() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
m.Subject = val
|
|
|
|
m.SubjectSrc = src
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetNotes updates the photo details field.
|
|
|
|
func (m *Details) SetNotes(data, src string) {
|
|
|
|
val := txt.Clip(data, txt.ClipDescription)
|
|
|
|
|
|
|
|
if val == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SrcPriority[src] < SrcPriority[m.NotesSrc]) && m.HasNotes() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
m.Notes = val
|
|
|
|
m.NotesSrc = src
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetArtist updates the photo details field.
|
|
|
|
func (m *Details) SetArtist(data, src string) {
|
|
|
|
val := txt.Clip(data, txt.ClipVarchar)
|
|
|
|
|
|
|
|
if val == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SrcPriority[src] < SrcPriority[m.ArtistSrc]) && m.HasArtist() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
m.Artist = val
|
|
|
|
m.ArtistSrc = src
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetCopyright updates the photo details field.
|
|
|
|
func (m *Details) SetCopyright(data, src string) {
|
|
|
|
val := txt.Clip(data, txt.ClipVarchar)
|
|
|
|
|
|
|
|
if val == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SrcPriority[src] < SrcPriority[m.CopyrightSrc]) && m.HasCopyright() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
m.Copyright = val
|
|
|
|
m.CopyrightSrc = src
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetLicense updates the photo details field.
|
|
|
|
func (m *Details) SetLicense(data, src string) {
|
|
|
|
val := txt.Clip(data, txt.ClipVarchar)
|
|
|
|
|
|
|
|
if val == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SrcPriority[src] < SrcPriority[m.LicenseSrc]) && m.HasLicense() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
m.License = val
|
|
|
|
m.LicenseSrc = src
|
|
|
|
}
|