Photos: Generate title based on estimated place #154
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
1df0d9a549
commit
301e510b2d
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
64
internal/entity/photo_maintenance.go
Normal file
64
internal/entity/photo_maintenance.go
Normal 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
|
||||
}
|
@ -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")
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user