diff --git a/internal/api/photo.go b/internal/api/photo.go index 4943e8b8d..51719ca7c 100644 --- a/internal/api/photo.go +++ b/internal/api/photo.go @@ -364,7 +364,7 @@ func PhotoFileUngroup(router *gin.RouterGroup) { existingPhoto := *file.Photo newPhoto := entity.NewPhoto() - if err := entity.UnscopedDb().Create(&newPhoto).Error; err != nil { + if err := newPhoto.Create(); err != nil { log.Errorf("photo: %s", err.Error()) AbortSaveFailed(c) return diff --git a/internal/api/photo_label.go b/internal/api/photo_label.go index cd23a00d7..dc35d13e5 100644 --- a/internal/api/photo_label.go +++ b/internal/api/photo_label.go @@ -75,7 +75,7 @@ func AddPhotoLabel(router *gin.RouterGroup) { return } - if err := p.Save(); err != nil { + if err := p.SaveLabels(); err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())}) return } @@ -139,7 +139,7 @@ func RemovePhotoLabel(router *gin.RouterGroup) { logError("label", p.RemoveKeyword(label.Label.LabelName)) - if err := p.Save(); err != nil { + if err := p.SaveLabels(); err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())}) return } @@ -206,7 +206,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup) { return } - if err := p.Save(); err != nil { + if err := p.SaveLabels(); err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())}) return } diff --git a/internal/entity/details.go b/internal/entity/details.go index 90f6eb1df..18521a5e5 100644 --- a/internal/entity/details.go +++ b/internal/entity/details.go @@ -1,14 +1,23 @@ package entity +import "time" + // Details stores additional metadata fields for each photo to improve search performance. type Details struct { - PhotoID uint `gorm:"primary_key;auto_increment:false" yaml:"-"` - Keywords string `gorm:"type:text;" json:"Keywords" yaml:"Keywords"` - Notes string `gorm:"type:text;" json:"Notes" yaml:"Notes,omitempty"` - Subject string `gorm:"type:varchar(255);" json:"Subject" yaml:"Subject,omitempty"` - Artist string `gorm:"type:varchar(255);" json:"Artist" yaml:"Artist,omitempty"` - Copyright string `gorm:"type:varchar(255);" json:"Copyright" yaml:"Copyright,omitempty"` - License string `gorm:"type:varchar(255);" json:"License" yaml:"License,omitempty"` + PhotoID uint `gorm:"primary_key;auto_increment:false" yaml:"-"` + Keywords string `gorm:"type:text;" json:"Keywords" yaml:"Keywords"` + Notes string `gorm:"type:text;" json:"Notes" yaml:"Notes,omitempty"` + Subject string `gorm:"type:varchar(255);" json:"Subject" yaml:"Subject,omitempty"` + Artist string `gorm:"type:varchar(255);" json:"Artist" yaml:"Artist,omitempty"` + Copyright string `gorm:"type:varchar(255);" json:"Copyright" yaml:"Copyright,omitempty"` + License string `gorm:"type:varchar(255);" json:"License" yaml:"License,omitempty"` + CreatedAt time.Time `yaml:"-"` + UpdatedAt time.Time `yaml:"-"` +} + +// NewDetails creates new photo details. +func NewDetails(photo Photo) Details { + return Details{PhotoID: photo.ID} } // Create inserts a new row to the database. @@ -21,6 +30,10 @@ func FirstOrCreateDetails(m *Details) *Details { result := Details{} if err := Db().Where("photo_id = ?", m.PhotoID).First(&result).Error; err == nil { + if m.CreatedAt.IsZero() { + m.CreatedAt = Timestamp() + } + return &result } else if err := m.Create(); err != nil { log.Errorf("details: %s", err) diff --git a/internal/entity/details_test.go b/internal/entity/details_test.go index 1ffa08eb9..88ad904bd 100644 --- a/internal/entity/details_test.go +++ b/internal/entity/details_test.go @@ -78,3 +78,22 @@ func TestDetails_NoCopyright(t *testing.T) { assert.Equal(t, false, description.NoCopyright()) }) } + +func TestNewDetails(t *testing.T) { + t.Run("add to photo", func(t *testing.T) { + p := NewPhoto() + d := NewDetails(p) + p.Details = &d + d.Subject = "Foo Bar" + d.Keywords = "Baz" + + err := p.Save() + + if err != nil { + t.Fatal(err) + } + + t.Logf("PHOTO: %#v", p) + t.Logf("DETAILS: %#v", d) + }) +} diff --git a/internal/entity/file.go b/internal/entity/file.go index 2e23c0cc3..0a6d41fd2 100644 --- a/internal/entity/file.go +++ b/internal/entity/file.go @@ -144,19 +144,22 @@ func (m *File) AllFilesMissing() bool { return count == 0 } +// Create inserts a new row to the database. +func (m *File) Create() error { + if m.PhotoID == 0 { + return fmt.Errorf("file: photo id is empty (create)") + } + + return UnscopedDb().Create(m).Error +} + // Saves the file in the database. func (m *File) Save() error { if m.PhotoID == 0 { return fmt.Errorf("file: photo id is empty (%s)", m.FileUID) } - if err := Db().Save(m).Error; err != nil { - return err - } - - photo := Photo{} - - return Db().Model(m).Related(&photo).Error + return UnscopedDb().Save(m).Error } // UpdateVideoInfos updates related video infos based on this file. diff --git a/internal/entity/photo.go b/internal/entity/photo.go index 0b101334e..b54513ed3 100644 --- a/internal/entity/photo.go +++ b/internal/entity/photo.go @@ -43,7 +43,6 @@ type Photo struct { TitleSrc string `gorm:"type:varbinary(8);" json:"TitleSrc" yaml:"TitleSrc,omitempty"` PhotoDescription string `gorm:"type:text;" json:"Description" yaml:"Description,omitempty"` DescriptionSrc string `gorm:"type:varbinary(8);" json:"DescriptionSrc" yaml:"DescriptionSrc,omitempty"` - Details Details `json:"Details" yaml:"Details"` PhotoPath string `gorm:"type:varbinary(768);index;" json:"Path" yaml:"-"` PhotoName string `gorm:"type:varbinary(255);" json:"Name" yaml:"-"` OriginalName string `gorm:"type:varbinary(768);" json:"OriginalName" yaml:"OriginalName,omitempty"` @@ -72,6 +71,7 @@ type Photo struct { CameraSerial string `gorm:"type:varbinary(255);" json:"CameraSerial" yaml:"CameraSerial,omitempty"` CameraSrc string `gorm:"type:varbinary(8);" json:"CameraSrc" yaml:"-"` LensID uint `gorm:"index:idx_photos_camera_lens;" json:"LensID" yaml:"-"` + Details *Details `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Details" yaml:"Details"` Camera *Camera `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Camera" yaml:"-"` Lens *Lens `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Lens" yaml:"-"` Location *Location `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Location" yaml:"-"` @@ -96,6 +96,10 @@ func NewPhoto() Photo { LensID: UnknownLens.ID, LocationID: UnknownLocation.ID, PlaceID: UnknownPlace.ID, + Camera: &UnknownCamera, + Lens: &UnknownLens, + Location: &UnknownLocation, + Place: &UnknownPlace, } } @@ -113,12 +117,14 @@ func SavePhotoForm(model Photo, form form.Photo, geoApi string) error { model.UpdateDateFields() + details := model.GetDetails() + if form.Details.PhotoID == model.ID { - if err := deepcopier.Copy(&model.Details).From(form.Details); err != nil { + if err := deepcopier.Copy(details).From(form.Details); err != nil { return err } - model.Details.Keywords = strings.Join(txt.UniqueWords(txt.Words(model.Details.Keywords)), ", ") + details.Keywords = strings.Join(txt.UniqueWords(txt.Words(details.Keywords)), ", ") } if locChanged && model.LocationSrc == SrcManual { @@ -126,10 +132,10 @@ func SavePhotoForm(model Photo, form form.Photo, geoApi string) error { model.AddLabels(labels) - w := txt.UniqueWords(txt.Words(model.Details.Keywords)) + w := txt.UniqueWords(txt.Words(details.Keywords)) w = append(w, locKeywords...) - model.Details.Keywords = strings.Join(txt.UniqueWords(w), ", ") + details.Keywords = strings.Join(txt.UniqueWords(w), ", ") } if err := model.SyncKeywordLabels(); err != nil { @@ -148,7 +154,7 @@ func SavePhotoForm(model Photo, form form.Photo, geoApi string) error { model.EditedAt = &edited model.PhotoQuality = model.QualityScore() - if err := UnscopedDb().Save(&model).Error; err != nil { + if err := model.Save(); err != nil { return err } @@ -174,8 +180,38 @@ func (m *Photo) String() string { return "uid " + txt.Quote(m.PhotoUID) } -// Save the entity in the database. +// Create inserts a new row to the database. +func (m *Photo) Create() error { + if err := UnscopedDb().Create(m).Error; err != nil { + log.Errorf("photo: %s (create)", err) + return err + } + + if err := UnscopedDb().Save(m.GetDetails()).Error; err != nil { + log.Errorf("photo: %s (save details after create)", err) + return err + } + + return nil +} + +// Save updates the existing or inserts a new row. func (m *Photo) Save() error { + if err := UnscopedDb().Save(m).Error; err != nil { + log.Errorf("photo: %s (save)", err) + return err + } + + if err := UnscopedDb().Save(m.GetDetails()).Error; err != nil { + log.Errorf("photo: %s (save details)", err) + return err + } + + return nil +} + +// Save the entity in the database. +func (m *Photo) SaveLabels() error { if !m.HasID() { return errors.New("photo: can't save to database, id is empty") } @@ -188,11 +224,11 @@ func (m *Photo) Save() error { log.Info(err) } - if m.DetailsLoaded() { - w := txt.UniqueWords(txt.Words(m.Details.Keywords)) - w = append(w, labels.Keywords()...) - m.Details.Keywords = strings.Join(txt.UniqueWords(w), ", ") - } + details := m.GetDetails() + + w := txt.UniqueWords(txt.Words(details.Keywords)) + w = append(w, labels.Keywords()...) + details.Keywords = strings.Join(txt.UniqueWords(w), ", ") if err := m.IndexKeywords(); err != nil { log.Errorf("photo: %s", err.Error()) @@ -200,7 +236,7 @@ func (m *Photo) Save() error { m.PhotoQuality = m.QualityScore() - if err := UnscopedDb().Save(m).Error; err != nil { + if err := m.Save(); err != nil { return err } @@ -267,20 +303,18 @@ func (m *Photo) BeforeSave(scope *gorm.Scope) error { // RemoveKeyword removes a word from photo keywords. func (m *Photo) RemoveKeyword(w string) error { - if !m.DetailsLoaded() { - return fmt.Errorf("can't remove keyword, details not loaded") - } + details := m.GetDetails() - words := txt.RemoveFromWords(txt.Words(m.Details.Keywords), w) - - m.Details.Keywords = strings.Join(words, ", ") + words := txt.RemoveFromWords(txt.Words(details.Keywords), w) + details.Keywords = strings.Join(words, ", ") return nil } // SyncKeywordLabels maintains the label / photo relationship for existing labels and keywords. func (m *Photo) SyncKeywordLabels() error { - keywords := txt.UniqueKeywords(m.Details.Keywords) + details := m.GetDetails() + keywords := txt.UniqueKeywords(details.Keywords) var labelIds []uint @@ -300,11 +334,8 @@ func (m *Photo) SyncKeywordLabels() error { // IndexKeywords adds given keywords to the photo entry func (m *Photo) IndexKeywords() error { - if !m.DetailsLoaded() { - return fmt.Errorf("can't index keywords, details not loaded") - } - - db := Db() + db := UnscopedDb() + details := m.GetDetails() var keywordIds []uint var keywords []string @@ -312,9 +343,9 @@ func (m *Photo) IndexKeywords() error { // Add title, description and other keywords keywords = append(keywords, txt.Keywords(m.PhotoTitle)...) keywords = append(keywords, txt.Keywords(m.PhotoDescription)...) - keywords = append(keywords, txt.Keywords(m.Details.Keywords)...) - keywords = append(keywords, txt.Keywords(m.Details.Subject)...) - keywords = append(keywords, txt.Keywords(m.Details.Artist)...) + keywords = append(keywords, txt.Keywords(details.Keywords)...) + keywords = append(keywords, txt.Keywords(details.Subject)...) + keywords = append(keywords, txt.Keywords(details.Artist)...) keywords = txt.UniqueWords(keywords) @@ -529,9 +560,19 @@ func (m *Photo) HasDescription() bool { return m.PhotoDescription != "" } -// DetailsLoaded returns true if photo details exist. -func (m *Photo) DetailsLoaded() bool { - return m.Details.PhotoID == m.ID +// GetDetails returns the photo description details. +func (m *Photo) GetDetails() *Details { + if m.Details == nil { + m.Details = &Details{PhotoID: m.ID} + } else { + return m.Details + } + + if details := FirstOrCreateDetails(m.Details); details != nil { + m.Details = details + } + + return m.Details } // FileTitle returns a photo title based on the file name and/or path. diff --git a/internal/entity/photo_fixtures.go b/internal/entity/photo_fixtures.go index a88760750..82d69de3f 100644 --- a/internal/entity/photo_fixtures.go +++ b/internal/entity/photo_fixtures.go @@ -58,7 +58,7 @@ var PhotoFixtures = PhotoMap{ TimeZone: "", PhotoYear: 2790, PhotoMonth: 2, - Details: DetailsFixtures.Get("lake", 1000000), + Details: DetailsFixtures.Pointer("lake", 1000000), DescriptionSrc: "", LocationID: UnknownLocation.ID, Location: &UnknownLocation, @@ -118,7 +118,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: UnknownPlace.CountryCode(), PhotoYear: 2790, PhotoMonth: 2, - Details: DetailsFixtures.Get("lake", 1000001), + Details: DetailsFixtures.Pointer("lake", 1000001), DescriptionSrc: "", Keywords: []Keyword{}, Albums: []Album{}, @@ -159,7 +159,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: UnknownPlace.CountryCode(), PhotoYear: 1990, PhotoMonth: 3, - Details: DetailsFixtures.Get("lake", 1000002), + Details: DetailsFixtures.Pointer("lake", 1000002), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -211,7 +211,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: LocationFixtures.Pointer("caravan park").Place.CountryCode(), PhotoYear: 1990, PhotoMonth: 4, - Details: DetailsFixtures.Get("bridge", 1000003), + Details: DetailsFixtures.Pointer("bridge", 1000003), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -263,7 +263,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("mexico").CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: DetailsFixtures.Get("lake", 1000004), + Details: DetailsFixtures.Pointer("lake", 1000004), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -315,7 +315,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("mexico").CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: DetailsFixtures.Get("lake", 1000005), + Details: DetailsFixtures.Pointer("lake", 1000005), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -363,7 +363,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: UnknownPlace.CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: DetailsFixtures.Get("lake", 1000006), + Details: DetailsFixtures.Pointer("lake", 1000006), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -411,7 +411,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: UnknownPlace.CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: DetailsFixtures.Get("lake", 1000007), + Details: DetailsFixtures.Pointer("lake", 1000007), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -454,7 +454,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("mexico").CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: DetailsFixtures.Get("lake", 1000008), + Details: DetailsFixtures.Pointer("lake", 1000008), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -502,7 +502,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("mexico").CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: DetailsFixtures.Get("lake", 1000009), + Details: DetailsFixtures.Pointer("lake", 1000009), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -550,7 +550,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("holidaypark").CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: DetailsFixtures.Get("lake", 10000010), + Details: DetailsFixtures.Pointer("lake", 10000010), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -598,7 +598,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("emptyNameLongCity").CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: DetailsFixtures.Get("lake", 10000011), + Details: DetailsFixtures.Pointer("lake", 10000011), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -655,7 +655,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("emptyNameShortCity").CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: Details{}, + Details: &Details{}, DescriptionSrc: "", Keywords: []Keyword{}, Albums: []Album{}, @@ -699,7 +699,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("veryLongLocName").CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: Details{}, + Details: &Details{}, DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -742,7 +742,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("mediumLongLocName").CountryCode(), PhotoYear: 2014, PhotoMonth: 7, - Details: Details{}, + Details: &Details{}, DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -796,7 +796,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: UnknownCountry.ID, PhotoYear: 0, PhotoMonth: 0, - Details: DetailsFixtures.Get("blacklist", 1000015), + Details: DetailsFixtures.Pointer("blacklist", 1000015), DescriptionSrc: "location", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, @@ -848,7 +848,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: UnknownCountry.ID, PhotoYear: 0, PhotoMonth: 0, - Details: DetailsFixtures.Get("lake", 1000015), + Details: DetailsFixtures.Pointer("lake", 1000015), DescriptionSrc: "location", Keywords: []Keyword{}, Albums: []Album{}, @@ -896,7 +896,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("mexico").CountryCode(), PhotoYear: 0, PhotoMonth: 0, - Details: DetailsFixtures.Get("lake", 1000015), + Details: DetailsFixtures.Pointer("lake", 1000015), DescriptionSrc: "location", Keywords: []Keyword{}, Albums: []Album{}, @@ -946,7 +946,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: PlaceFixtures.Pointer("mexico").CountryCode(), PhotoYear: 0, PhotoMonth: 0, - Details: DetailsFixtures.Get("lake", 1000015), + Details: DetailsFixtures.Pointer("lake", 1000015), DescriptionSrc: "location", Keywords: []Keyword{}, Albums: []Album{}, @@ -992,7 +992,7 @@ var PhotoFixtures = PhotoMap{ PhotoCountry: UnknownPlace.CountryCode(), PhotoYear: 1990, PhotoMonth: 4, - Details: DetailsFixtures.Get("bridge", 1000019), + Details: DetailsFixtures.Pointer("bridge", 1000019), DescriptionSrc: "", Camera: CameraFixtures.Pointer("canon-eos-6d"), CameraID: CameraFixtures.Pointer("canon-eos-6d").ID, diff --git a/internal/entity/photo_optimize.go b/internal/entity/photo_optimize.go index 281e569b0..767976694 100644 --- a/internal/entity/photo_optimize.go +++ b/internal/entity/photo_optimize.go @@ -111,11 +111,10 @@ func (m *Photo) Optimize() (updated bool, err error) { log.Info(err) } - if m.DetailsLoaded() { - w := txt.UniqueWords(txt.Words(m.Details.Keywords)) - w = append(w, labels.Keywords()...) - m.Details.Keywords = strings.Join(txt.UniqueWords(w), ", ") - } + details := m.GetDetails() + w := txt.UniqueWords(txt.Words(details.Keywords)) + w = append(w, labels.Keywords()...) + details.Keywords = strings.Join(txt.UniqueWords(w), ", ") if err := m.IndexKeywords(); err != nil { log.Errorf("photo: %s", err.Error()) diff --git a/internal/entity/photo_quality.go b/internal/entity/photo_quality.go index 009a24e97..8528f58b0 100644 --- a/internal/entity/photo_quality.go +++ b/internal/entity/photo_quality.go @@ -42,8 +42,10 @@ func (m *Photo) QualityScore() (score int) { blacklisted := false - if m.Details.Keywords != "" { - keywords := txt.Words(m.Details.Keywords) + details := m.GetDetails() + + if details.Keywords != "" { + keywords := txt.Words(details.Keywords) for _, w := range keywords { w = strings.ToLower(w) diff --git a/internal/entity/photo_test.go b/internal/entity/photo_test.go index b4afad93b..0bf08203a 100644 --- a/internal/entity/photo_test.go +++ b/internal/entity/photo_test.go @@ -65,7 +65,8 @@ func TestSavePhotoForm(t *testing.T) { assert.Equal(t, "image", m.PhotoType) assert.Equal(t, float32(7.9999), m.PhotoLat) assert.NotNil(t, m.EditedAt) - t.Log(m.Details.Keywords) + + t.Log(m.GetDetails().Keywords) }) } @@ -97,7 +98,7 @@ func TestPhoto_Save(t *testing.T) { PlaceID: "765", PhotoCountry: "de", Keywords: []Keyword{}, - Details: Details{ + Details: &Details{ PhotoID: 11111, Keywords: "test cat dog", Subject: "animals", @@ -108,14 +109,14 @@ func TestPhoto_Save(t *testing.T) { }, } - err := photo.Save() + err := photo.SaveLabels() assert.EqualError(t, err, "photo: can't save to database, id is empty") }) t.Run("existing photo", func(t *testing.T) { m := PhotoFixtures.Get("19800101_000002_D640C559") - err := m.Save() + err := m.SaveLabels() if err != nil { t.Fatal(err) } @@ -280,14 +281,30 @@ func TestPhoto_NoCameraSerial(t *testing.T) { }) } -func TestPhoto_DetailsLoaded(t *testing.T) { +func TestPhoto_GetDetails(t *testing.T) { t.Run("true", func(t *testing.T) { m := PhotoFixtures.Get("19800101_000002_D640C559") - assert.True(t, m.DetailsLoaded()) + result := m.GetDetails() + + if result == nil { + t.Fatal("result should never be nil") + } + + if result.PhotoID == 0 { + t.Fatal("PhotoID should not be 0") + } }) t.Run("false", func(t *testing.T) { m := PhotoFixtures.Get("Photo12") - assert.False(t, m.DetailsLoaded()) + result := m.GetDetails() + + if result == nil { + t.Fatal("result should never be nil") + } + + if result.PhotoID != 0 { + t.Fatal("PhotoID should be 0") + } }) } diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index a3f41fa83..e3521e9e5 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -65,7 +65,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( photo := entity.NewPhoto() metaData := meta.Data{} - description := entity.Details{} labels := classify.Labels{} fileRoot, fileBase, filePath, fileName := m.PathNameInfo() @@ -134,9 +133,9 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( return result } - if photoExists { - entity.UnscopedDb().Model(&photo).Related(&description) - } else { + details := photo.GetDetails() + + if !photoExists { photo.PhotoQuality = -1 if yamlName := fs.TypeYaml.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); yamlName != "" { @@ -214,16 +213,16 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( photo.SetTitle(data.Title, entity.SrcXmp) photo.SetDescription(data.Description, entity.SrcXmp) - if photo.Details.NoNotes() && data.Comment != "" { - photo.Details.Notes = data.Comment + if details.NoNotes() && data.Comment != "" { + details.Notes = data.Comment } - if photo.Details.NoArtist() && data.Artist != "" { - photo.Details.Artist = data.Artist + if details.NoArtist() && data.Artist != "" { + details.Artist = data.Artist } - if photo.Details.NoCopyright() && data.Copyright != "" { - photo.Details.Copyright = data.Copyright + if details.NoCopyright() && data.Copyright != "" { + details.Copyright = data.Copyright } } case m.IsRaw(), m.IsHEIF(), m.IsImageOther(): @@ -233,24 +232,24 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcMeta) photo.SetCoordinates(metaData.Lat, metaData.Lng, metaData.Altitude, entity.SrcMeta) - if photo.Details.NoNotes() { - photo.Details.Notes = metaData.Comment + if details.NoNotes() { + details.Notes = metaData.Comment } - if photo.Details.NoSubject() { - photo.Details.Subject = metaData.Subject + if details.NoSubject() { + details.Subject = metaData.Subject } - if photo.Details.NoKeywords() { - photo.Details.Keywords = metaData.Keywords + if details.NoKeywords() { + details.Keywords = metaData.Keywords } - if photo.Details.NoArtist() && metaData.Artist != "" { - photo.Details.Artist = metaData.Artist + if details.NoArtist() && metaData.Artist != "" { + details.Artist = metaData.Artist } - if photo.Details.NoArtist() && metaData.CameraOwner != "" { - photo.Details.Artist = metaData.CameraOwner + if details.NoArtist() && metaData.CameraOwner != "" { + details.Artist = metaData.CameraOwner } if photo.NoCameraSerial() { @@ -290,24 +289,24 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcMeta) photo.SetCoordinates(metaData.Lat, metaData.Lng, metaData.Altitude, entity.SrcMeta) - if photo.Details.NoNotes() { - photo.Details.Notes = metaData.Comment + if details.NoNotes() { + details.Notes = metaData.Comment } - if photo.Details.NoSubject() { - photo.Details.Subject = metaData.Subject + if details.NoSubject() { + details.Subject = metaData.Subject } - if photo.Details.NoKeywords() { - photo.Details.Keywords = metaData.Keywords + if details.NoKeywords() { + details.Keywords = metaData.Keywords } - if photo.Details.NoArtist() && metaData.Artist != "" { - photo.Details.Artist = metaData.Artist + if details.NoArtist() && metaData.Artist != "" { + details.Artist = metaData.Artist } - if photo.Details.NoArtist() && metaData.CameraOwner != "" { - photo.Details.Artist = metaData.CameraOwner + if details.NoArtist() && metaData.CameraOwner != "" { + details.Artist = metaData.CameraOwner } if photo.NoCameraSerial() { @@ -385,24 +384,24 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcMeta) photo.SetCoordinates(metaData.Lat, metaData.Lng, metaData.Altitude, entity.SrcMeta) - if photo.Details.NoNotes() { - photo.Details.Notes = metaData.Comment + if details.NoNotes() { + details.Notes = metaData.Comment } - if photo.Details.NoSubject() { - photo.Details.Subject = metaData.Subject + if details.NoSubject() { + details.Subject = metaData.Subject } - if photo.Details.NoKeywords() { - photo.Details.Keywords = metaData.Keywords + if details.NoKeywords() { + details.Keywords = metaData.Keywords } - if photo.Details.NoArtist() && metaData.Artist != "" { - photo.Details.Artist = metaData.Artist + if details.NoArtist() && metaData.Artist != "" { + details.Artist = metaData.Artist } - if photo.Details.NoArtist() && metaData.CameraOwner != "" { - photo.Details.Artist = metaData.CameraOwner + if details.NoArtist() && metaData.CameraOwner != "" { + details.Artist = metaData.CameraOwner } if photo.NoCameraSerial() { @@ -480,14 +479,14 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( file.FileOrientation = m.Orientation() if photoExists { - if err := entity.UnscopedDb().Save(&photo).Error; err != nil { + if err := photo.Save(); err != nil { log.Errorf("index: %s for %s", err.Error(), logName) result.Status = IndexFailed result.Error = err return result } } else { - if err := entity.UnscopedDb().Create(&photo).Error; err != nil { + if err := photo.Create(); err != nil { log.Errorf("index: %s", err) result.Status = IndexFailed result.Error = err @@ -529,7 +528,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( log.Debugf("%s (%s)", err.Error(), logName) } - w := txt.Keywords(photo.Details.Keywords) + w := txt.Keywords(details.Keywords) if !fs.IsID(fileBase) { w = append(w, txt.FilenameKeywords(filePath)...) @@ -541,17 +540,17 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( w = append(w, file.FileMainColor) w = append(w, labels.Keywords()...) - photo.Details.Keywords = strings.Join(txt.UniqueWords(w), ", ") + details.Keywords = strings.Join(txt.UniqueWords(w), ", ") - if photo.Details.Keywords != "" { - log.Tracef("index: set keywords %s for %s", photo.Details.Keywords, logName) + if details.Keywords != "" { + log.Tracef("index: set keywords %s for %s", details.Keywords, logName) } else { log.Tracef("index: no keywords for %s", logName) } photo.PhotoQuality = photo.QualityScore() - if err := entity.UnscopedDb().Save(&photo).Error; err != nil { + if err := photo.Save(); err != nil { log.Errorf("index: %s for %s", err, logName) result.Status = IndexFailed result.Error = err @@ -570,7 +569,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( photo.PhotoQuality = photo.QualityScore() } - if err := entity.UnscopedDb().Unscoped().Save(&photo).Error; err != nil { + if err := photo.Save(); err != nil { log.Errorf("index: %s for %s", err, logName) result.Status = IndexFailed result.Error = err @@ -583,7 +582,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( if fileQuery.Error == nil { file.UpdatedIn = int64(time.Since(start)) - if err := entity.UnscopedDb().Save(&file).Error; err != nil { + if err := file.Save(); err != nil { log.Errorf("index: %s for %s", err, logName) result.Status = IndexFailed result.Error = err @@ -592,7 +591,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( } else { file.CreatedIn = int64(time.Since(start)) - if err := entity.UnscopedDb().Create(&file).Error; err != nil { + if err := file.Create(); err != nil { log.Errorf("index: %s for %s", err, logName) result.Status = IndexFailed result.Error = err