diff --git a/internal/entity/location.go b/internal/entity/location.go index 21cf88701..2f212c482 100644 --- a/internal/entity/location.go +++ b/internal/entity/location.go @@ -159,7 +159,7 @@ func (m *Location) Keywords() (result []string) { // Unknown checks if the location has no id func (m *Location) Unknown() bool { - return m.LocUID == "" + return m.LocUID == "" || m.LocUID == UnknownLocation.LocUID } // Name returns name of location diff --git a/internal/entity/photo.go b/internal/entity/photo.go index bbcd3bded..c78980095 100644 --- a/internal/entity/photo.go +++ b/internal/entity/photo.go @@ -103,7 +103,7 @@ func SavePhotoForm(model Photo, form form.Photo, geoApi string) error { } if err := model.UpdateTitle(model.ClassifyLabels()); err != nil { - log.Warnf("%s (%s)", err.Error(), model.PhotoUID) + log.Warn(err) } if err := model.IndexKeywords(); err != nil { @@ -136,7 +136,7 @@ func (m *Photo) Save() error { m.UpdateYearMonth() if err := m.UpdateTitle(labels); err != nil { - log.Warnf("%s (%s)", err.Error(), m.PhotoUID) + log.Warn(err) } if m.DetailsLoaded() { @@ -219,7 +219,7 @@ func (m *Photo) BeforeSave(scope *gorm.Scope) error { // IndexKeywords adds given keywords to the photo entry func (m *Photo) IndexKeywords() error { if !m.DetailsLoaded() { - return fmt.Errorf("photo: can't index keywords, details not loaded (%s)", m.PhotoUID) + return fmt.Errorf("photo: can't index keywords, details not loaded for %s", m.PhotoUID) } db := Db() @@ -326,7 +326,7 @@ func (m *Photo) HasLocation() bool { // LocationLoaded checks if the photo has a known location that is currently loaded. func (m *Photo) LocationLoaded() bool { - return m.Location != nil && m.Location.Place != nil && m.Location.LocUID != UnknownLocation.LocUID + return m.Location != nil && m.Location.Place != nil && !m.Location.Unknown() } // HasLatLng checks if the photo has a latitude and longitude. @@ -339,6 +339,11 @@ func (m *Photo) NoLatLng() bool { return !m.HasLatLng() } +// PlaceLoaded checks if the photo has a known place that is currently loaded. +func (m *Photo) PlaceLoaded() bool { + return m.Place != nil && !m.Place.Unknown() +} + // NoPlace checks if the photo has an unknown place. func (m *Photo) NoPlace() bool { return m.PlaceUID == "" || m.PlaceUID == UnknownPlace.PlaceUID @@ -377,12 +382,13 @@ func (m *Photo) DetailsLoaded() bool { // UpdateTitle updated the photo title based on location and labels. func (m *Photo) UpdateTitle(labels classify.Labels) error { if m.TitleSrc != SrcAuto && m.HasTitle() { - return errors.New("photo: won't update title, was modified") + return fmt.Errorf("photo: won't update title, %s was modified", m.PhotoUID) } - knownLocation := m.LocationLoaded() + var knownLocation bool - if knownLocation { + if m.LocationLoaded() { + knownLocation = true loc := m.Location if title := labels.Title(loc.Name()); title != "" { // TODO: User defined title format @@ -407,6 +413,23 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error { m.SetTitle(fmt.Sprintf("%s / %s / %s", loc.City(), loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto) } } + } else if m.PlaceLoaded() { + knownLocation = true + + if title := labels.Title(""); title != "" { + log.Infof("photo: using label %s to create photo title", txt.Quote(title)) + if m.Place.NoCity() || m.Place.LongCity() || m.Place.CityContains(title) { + m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), m.Place.CountryName(), m.TakenAt.Format("2006")), SrcAuto) + } else { + m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), m.Place.City(), m.TakenAt.Format("2006")), SrcAuto) + } + } else if m.Place.City() != "" && m.Place.CountryName() != "" { + if len(m.Place.City()) > 20 { + m.SetTitle(fmt.Sprintf("%s / %s", m.Place.City(), m.TakenAt.Format("2006")), SrcAuto) + } else { + m.SetTitle(fmt.Sprintf("%s / %s / %s", m.Place.City(), m.Place.CountryName(), m.TakenAt.Format("2006")), SrcAuto) + } + } } if !knownLocation || m.NoTitle() { @@ -631,57 +654,3 @@ func (m *Photo) SetFavorite(favorite bool) error { return nil } - -// EstimatePosition updates the photo with an estimated geolocation if possible. -func (m *Photo) EstimatePosition() { - var recentPhoto Photo - - if result := UnscopedDb(). - Where("place_uid <> '' && place_uid <> 'zz'"). - Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", m.TakenAt)). - Preload("Place").First(&recentPhoto); result.Error == nil { - if recentPhoto.HasPlace() { - m.Place = recentPhoto.Place - m.PlaceUID = recentPhoto.PlaceUID - m.PhotoCountry = recentPhoto.PhotoCountry - m.LocSrc = SrcEstimate - log.Debugf("prism: approximate location for %s is %s", m.PhotoUID, recentPhoto.PlaceUID) - } - } -} - -// Maintain photo data, improve if possible. -func (m *Photo) Maintain() error { - if !m.HasID() { - return errors.New("photo: can't maintain, id is empty") - } - - maintained := time.Now() - m.MaintainedAt = &maintained - - if m.NoPlace() && (m.LocSrc == SrcAuto || m.LocSrc == SrcEstimate) { - m.EstimatePosition() - } - - labels := m.ClassifyLabels() - - m.UpdateYearMonth() - - if err := m.UpdateTitle(labels); err != nil { - log.Warnf("%s (%s)", err.Error(), m.PhotoUID) - } - - if m.DetailsLoaded() { - w := txt.UniqueKeywords(m.Details.Keywords) - w = append(w, labels.Keywords()...) - m.Details.Keywords = strings.Join(txt.UniqueWords(w), ", ") - } - - if err := m.IndexKeywords(); err != nil { - log.Error(err) - } - - m.PhotoQuality = m.QualityScore() - - return UnscopedDb().Save(m).Error -} diff --git a/internal/entity/photo_maintenance.go b/internal/entity/photo_maintenance.go new file mode 100644 index 000000000..5b257d6ec --- /dev/null +++ b/internal/entity/photo_maintenance.go @@ -0,0 +1,64 @@ +package entity + +import ( + "errors" + "strings" + "time" + + "github.com/jinzhu/gorm" + "github.com/photoprism/photoprism/pkg/txt" +) + +// EstimatePosition updates the photo with an estimated geolocation if possible. +func (m *Photo) EstimatePosition() { + var recentPhoto Photo + + if result := UnscopedDb(). + Where("place_uid <> '' && place_uid <> 'zz'"). + Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", m.TakenAt)). + Preload("Place").First(&recentPhoto); result.Error == nil { + if recentPhoto.HasPlace() { + m.Place = recentPhoto.Place + m.PlaceUID = recentPhoto.PlaceUID + m.PhotoCountry = recentPhoto.PhotoCountry + m.LocSrc = SrcEstimate + log.Debugf("prism: approximate position of %s is %s", m.PhotoUID, recentPhoto.PlaceUID) + } + } +} + +// Maintain photo data, improve if possible. +func (m *Photo) Maintain() error { + if !m.HasID() { + return errors.New("photo: can't maintain, id is empty") + } + + maintained := time.Now() + m.MaintainedAt = &maintained + + if m.NoLocation() && (m.LocSrc == SrcAuto || m.LocSrc == SrcEstimate) { + m.EstimatePosition() + } + + labels := m.ClassifyLabels() + + m.UpdateYearMonth() + + if err := m.UpdateTitle(labels); err != nil { + log.Warn(err) + } + + if m.DetailsLoaded() { + w := txt.UniqueKeywords(m.Details.Keywords) + w = append(w, labels.Keywords()...) + m.Details.Keywords = strings.Join(txt.UniqueWords(w), ", ") + } + + if err := m.IndexKeywords(); err != nil { + log.Error(err) + } + + m.PhotoQuality = m.QualityScore() + + return UnscopedDb().Save(m).Error +} diff --git a/internal/entity/photo_test.go b/internal/entity/photo_test.go index f1ac98429..a127efe4e 100644 --- a/internal/entity/photo_test.go +++ b/internal/entity/photo_test.go @@ -301,7 +301,6 @@ func TestPhoto_UpdateTitle(t *testing.T) { t.Fatal() } assert.Equal(t, "Black beach", m.PhotoTitle) - assert.Equal(t, "photo: won't update title, was modified", err.Error()) }) t.Run("photo with location without city and label", func(t *testing.T) { m := PhotoFixtures.Get("Photo10") diff --git a/internal/entity/place.go b/internal/entity/place.go index 65ba80d0a..2ea400032 100644 --- a/internal/entity/place.go +++ b/internal/entity/place.go @@ -1,6 +1,7 @@ package entity import ( + "strings" "time" "github.com/jinzhu/gorm" @@ -105,7 +106,7 @@ func FirstOrCreatePlace(m *Place) *Place { // Unknown returns true if this is an unknown place func (m Place) Unknown() bool { - return m.PlaceUID == UnknownPlace.PlaceUID + return m.PlaceUID == "" || m.PlaceUID == UnknownPlace.PlaceUID } // Label returns place label @@ -118,6 +119,21 @@ func (m Place) City() string { return m.LocCity } +// LongCity checks if the city name is more than 16 char. +func (m Place) LongCity() bool { + return len(m.LocCity) > 16 +} + +// NoCity checks if the location has no city +func (m Place) NoCity() bool { + return m.LocCity == "" +} + +// CityContains checks if the location city contains the text string +func (m Place) CityContains(text string) bool { + return strings.Contains(text, m.LocCity) +} + // State returns place State func (m Place) State() string { return m.LocState