Photos: Generate title based on estimated place #154

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-05-26 21:51:34 +02:00
parent 1df0d9a549
commit 301e510b2d
5 changed files with 112 additions and 64 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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")

View File

@ -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