Metadata: Remove location labels when photo doesn't have a location

This commit is contained in:
Michael Mayer 2021-11-25 14:52:26 +01:00
parent 08fe7326e5
commit ff66b3bccc
9 changed files with 88 additions and 32 deletions

View file

@ -1,5 +1,5 @@
/*
Package entity contains models for data storage based on GORM.
Package entity provides models for storing index information based on the GORM library.
See http://gorm.io/docs/ for more information about GORM.
@ -10,16 +10,15 @@ https://github.com/photoprism/photoprism/wiki/Storage
package entity
import (
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/event"
)
var log = event.Log
var GeoApi = "places"
// logError logs the message if the argument is an error.
func logError(result *gorm.DB) {
if result.Error != nil {
log.Error(result.Error.Error())
// Log logs the error if any and keeps quiet otherwise.
func Log(model, action string, err error) {
if err != nil {
log.Errorf("%s: %s (%s)", model, err, action)
}
}

View file

@ -179,7 +179,7 @@ func SavePhotoForm(model Photo, form form.Photo) error {
}
if err := model.SyncKeywordLabels(); err != nil {
log.Errorf("photo %s: %s while syncing keywords and labels", model.PhotoUID, err)
log.Errorf("photo: %s %s while syncing keywords and labels", model.String(), err)
}
if err := model.UpdateTitle(model.ClassifyLabels()); err != nil {
@ -187,7 +187,7 @@ func SavePhotoForm(model Photo, form form.Photo) error {
}
if err := model.IndexKeywords(); err != nil {
log.Errorf("photo %s: %s while indexing keywords", model.PhotoUID, err.Error())
log.Errorf("photo: %s %s while indexing keywords", model.String(), err.Error())
}
edited := TimeStamp()
@ -465,7 +465,7 @@ func (m *Photo) PreloadFiles() {
Where("files.photo_id = ? AND files.deleted_at IS NULL", m.ID).
Order("files.file_name DESC")
logError(q.Scan(&m.Files))
Log("photo", "preload files", q.Scan(&m.Files).Error)
}
// PreloadKeywords prepares gorm scope to retrieve photo keywords
@ -476,7 +476,7 @@ func (m *Photo) PreloadKeywords() {
Joins("JOIN photos_keywords ON photos_keywords.keyword_id = keywords.id AND photos_keywords.photo_id = ?", m.ID).
Order("keywords.keyword ASC")
logError(q.Scan(&m.Keywords))
Log("photo", "preload files", q.Scan(&m.Keywords).Error)
}
// PreloadAlbums prepares gorm scope to retrieve photo albums
@ -488,7 +488,7 @@ func (m *Photo) PreloadAlbums() {
Where("albums.deleted_at IS NULL").
Order("albums.album_title ASC")
logError(q.Scan(&m.Albums))
Log("photo", "preload albums", q.Scan(&m.Albums).Error)
}
// PreloadMany prepares gorm scope to retrieve photo file, albums and keywords
@ -613,7 +613,7 @@ func (m *Photo) SetDescription(desc, source string) {
// SetCamera updates the camera.
func (m *Photo) SetCamera(camera *Camera, source string) {
if camera == nil {
log.Warnf("photo %s: failed updating camera from source %s", txt.Quote(m.PhotoUID), SrcString(source))
log.Warnf("photo: %s failed updating camera from source %s", m.String(), SrcString(source))
return
}
@ -633,7 +633,7 @@ func (m *Photo) SetCamera(camera *Camera, source string) {
// SetLens updates the lens.
func (m *Photo) SetLens(lens *Lens, source string) {
if lens == nil {
log.Warnf("photo %s: failed updating lens from source %s", txt.Quote(m.PhotoUID), SrcString(source))
log.Warnf("photo: %s failed updating lens from source %s", m.String(), SrcString(source))
return
}

View file

@ -73,12 +73,14 @@ func (m *Photo) EstimateLocation(force bool) {
// Don't estimate if it seems to be a non-photographic image.
if m.UnknownCamera() && m.PhotoType == TypeImage {
m.RemoveLocation(SrcEstimate, false)
m.RemoveLocationLabels()
return
}
// Estimate country if TakenAt is unreliable.
if SrcPriority[m.TakenSrc] <= SrcPriority[SrcName] {
m.RemoveLocation(SrcEstimate, false)
m.RemoveLocationLabels()
m.EstimateCountry()
return
}
@ -119,12 +121,14 @@ func (m *Photo) EstimateLocation(force bool) {
if len(mostRecent) == 0 {
log.Debugf("photo: unknown position at %s", m.TakenAt)
m.RemoveLocation(SrcEstimate, false)
m.RemoveLocationLabels()
m.EstimateCountry()
} else if recentPhoto := mostRecent[0]; recentPhoto.HasLocation() && recentPhoto.HasPlace() {
// Too much time difference?
if hours := recentPhoto.TakenAt.Sub(m.TakenAt) / time.Hour; hours < -36 || hours > 36 {
log.Debugf("photo: skipping %s, %d hours time difference to recent position", m, hours)
m.RemoveLocation(SrcEstimate, false)
m.RemoveLocationLabels()
m.EstimateCountry()
} else if len(mostRecent) == 1 || m.UnknownCamera() {
m.AdoptPlace(recentPhoto, SrcEstimate, false)
@ -144,12 +148,14 @@ func (m *Photo) EstimateLocation(force bool) {
} else if recentPhoto.HasCountry() {
log.Debugf("photo: estimated country for %s is %s", m, txt.Quote(m.CountryName()))
m.RemoveLocation(SrcEstimate, false)
m.RemoveLocationLabels()
m.PhotoCountry = recentPhoto.PhotoCountry
m.PlaceSrc = SrcEstimate
m.UpdateTimeZone(recentPhoto.TimeZone)
} else {
log.Warnf("photo: %s has no location, uid %s", recentPhoto.PhotoName, recentPhoto.PhotoUID)
m.RemoveLocation(SrcEstimate, false)
m.RemoveLocationLabels()
m.EstimateCountry()
}
}

View file

@ -39,7 +39,7 @@ func (m *PhotoLabel) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error
}
// Updates a column in the database.
// Update a column in the database.
func (m *PhotoLabel) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
}
@ -62,6 +62,11 @@ func (m *PhotoLabel) Create() error {
return Db().Create(m).Error
}
// Delete deletes the label reference.
func (m *PhotoLabel) Delete() error {
return Db().Delete(m).Error
}
// FirstOrCreatePhotoLabel returns the existing row, inserts a new row or nil in case of errors.
func FirstOrCreatePhotoLabel(m *PhotoLabel) *PhotoLabel {
result := PhotoLabel{}

View file

@ -5,6 +5,8 @@ import (
"strings"
"time"
"github.com/dustin/go-humanize/english"
"github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/maps"
"github.com/photoprism/photoprism/pkg/geo"
@ -86,6 +88,13 @@ func (m *Photo) AdoptPlace(other Photo, source string, force bool) {
return
} else if other.Place == nil {
return
} else if other.Place.Unknown() {
return
}
// Remove existing location labels if place changes.
if other.Place.ID != m.PlaceID {
m.RemoveLocationLabels()
}
m.RemoveLocation(source, force)
@ -126,6 +135,41 @@ func (m *Photo) RemoveLocation(source string, force bool) {
m.PlaceSrc = SrcAuto
}
// RemoveLocationLabels removes existing location labels.
func (m *Photo) RemoveLocationLabels() {
if len(m.Labels) == 0 {
res := Db().Delete(PhotoLabel{}, "photo_id = ? AND label_src = ?", m.ID, SrcLocation)
if res.Error != nil {
Log("photo", "remove location labels", res.Error)
} else if res.RowsAffected > 0 {
log.Infof("photo: removed %s from %s",
english.Plural(int(res.RowsAffected), "location label", "location labels"), m)
}
return
}
labels := make([]PhotoLabel, 0, len(m.Labels))
for _, l := range m.Labels {
if l.LabelSrc != SrcLocation {
labels = append(labels, l)
continue
}
Log("photo", "remove location label", l.Delete())
}
removed := len(m.Labels) - len(labels)
if removed > 0 {
log.Infof("photo: removed %s from %s",
english.Plural(removed, "location label", "location labels"), m)
m.Labels = labels
}
}
// HasLocation tests if the photo has a known location.
func (m *Photo) HasLocation() bool {
return !m.UnknownLocation()
@ -327,6 +371,7 @@ func (m *Photo) UpdateLocation() (keywords []string, labels classify.Labels) {
if changed {
log.Debugf("photo: changing location of %s from %s to %s", m.String(), m.CellID, location.ID)
m.RemoveLocationLabels()
}
m.Cell = location
@ -404,7 +449,7 @@ func (m *Photo) SaveLocation() error {
m.GetDetails().Keywords = strings.Join(txt.UniqueWords(w), ", ")
if err := m.SyncKeywordLabels(); err != nil {
log.Errorf("photo %s: %s while syncing keywords and labels", m.PhotoUID, err)
log.Errorf("photo: %s %s while syncing keywords and labels", m.String(), err)
}
if err := m.UpdateTitle(m.ClassifyLabels()); err != nil {
@ -412,7 +457,7 @@ func (m *Photo) SaveLocation() error {
}
if err := m.IndexKeywords(); err != nil {
log.Errorf("photo %s: %s while indexing keywords", m.PhotoUID, err)
log.Errorf("photo: %s %s while indexing keywords", m.String(), err)
}
return m.Save()

View file

@ -29,24 +29,25 @@ func TestPhoto_SetPosition(t *testing.T) {
}
func TestPhoto_AdoptPlace(t *testing.T) {
place := PlaceFixtures.Get("mexico")
t.Run("SrcAuto", func(t *testing.T) {
p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: -1, PhotoLng: 1, PlaceSrc: SrcAuto}
o := Photo{ID: 1, Place: &UnknownPlace, PlaceID: UnknownPlace.ID, CellID: "s2:479a03fda18c", PhotoLat: 15, PhotoLng: -11, PlaceSrc: SrcManual}
o := Photo{ID: 1, Place: &place, PlaceID: place.ID, CellID: "s2:479a03fda18c", PhotoLat: 15, PhotoLng: -11, PlaceSrc: SrcManual}
assert.Nil(t, p.Place)
assert.Equal(t, "", p.PlaceID)
assert.Equal(t, "s2:479a03fda123", p.CellID)
assert.Equal(t, -1, int(p.PhotoLat))
assert.Equal(t, 1, int(p.PhotoLng))
p.AdoptPlace(o, SrcEstimate, false)
assert.Equal(t, &UnknownPlace, p.Place)
assert.Equal(t, UnknownPlace.ID, p.PlaceID)
assert.Equal(t, &place, p.Place)
assert.Equal(t, place.ID, p.PlaceID)
assert.Equal(t, "zz", p.CellID)
assert.Equal(t, 0, int(p.PhotoLat))
assert.Equal(t, 0, int(p.PhotoLng))
})
t.Run("SrcManual", func(t *testing.T) {
p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: 0, PhotoLng: 0, PlaceSrc: SrcManual}
o := Photo{ID: 1, Place: &UnknownPlace, PlaceID: UnknownPlace.ID, CellID: "s2:479a03fda18c", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcManual}
o := Photo{ID: 1, Place: &place, PlaceID: place.ID, CellID: "s2:479a03fda18c", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcManual}
assert.Nil(t, p.Place)
assert.Equal(t, "", p.PlaceID)
assert.Equal(t, "s2:479a03fda123", p.CellID)
@ -61,15 +62,15 @@ func TestPhoto_AdoptPlace(t *testing.T) {
})
t.Run("Force", func(t *testing.T) {
p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcManual}
o := Photo{ID: 1, Place: &UnknownPlace, PlaceID: UnknownPlace.ID, CellID: "s2:479a03fda18c", PhotoLat: 0, PhotoLng: 0, PlaceSrc: SrcManual}
o := Photo{ID: 1, Place: &place, PlaceID: place.ID, CellID: "s2:479a03fda18c", PhotoLat: 0, PhotoLng: 0, PlaceSrc: SrcManual}
assert.Nil(t, p.Place)
assert.Equal(t, "", p.PlaceID)
assert.Equal(t, "s2:479a03fda123", p.CellID)
assert.Equal(t, 1, int(p.PhotoLat))
assert.Equal(t, -1, int(p.PhotoLng))
p.AdoptPlace(o, SrcEstimate, true)
assert.Equal(t, &UnknownPlace, p.Place)
assert.Equal(t, UnknownPlace.ID, p.PlaceID)
assert.Equal(t, &place, p.Place)
assert.Equal(t, place.ID, p.PlaceID)
assert.Equal(t, "zz", p.CellID)
assert.Equal(t, 0, int(p.PhotoLat))
assert.Equal(t, 0, int(p.PhotoLng))

View file

@ -42,7 +42,7 @@ func (m *Photo) SetTitle(title, source string) {
// 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 fmt.Errorf("photo %s: keeping %s title %s", txt.Quote(m.PhotoUID), SrcString(m.TitleSrc), txt.Quote(m.PhotoTitle))
return fmt.Errorf("photo: %s keeps existing %s title", m.String(), SrcString(m.TitleSrc))
}
var names string
@ -66,7 +66,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
// TODO: User defined title format
if names != "" {
log.Debugf("photo %s: generating title from %s (%s)", txt.Quote(m.PhotoUID), english.Plural(len(people), "person", "people"), txt.Quote(names))
log.Debugf("photo: %s generating title from %s (%s)", m.String(), english.Plural(len(people), "person", "people"), txt.Quote(names))
if l := len([]rune(names)); l > 35 {
m.SetTitle(names, SrcAuto)
@ -80,7 +80,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
m.SetTitle(fmt.Sprintf("%s / %s / %s", names, loc.City(), m.TakenAt.Format("2006")), SrcAuto)
}
} else if title := labels.Title(loc.Name()); title != "" {
log.Debugf("photo %s: generating title from label %s", txt.Quote(m.PhotoUID), txt.Quote(title))
log.Debugf("photo: %s generating title from label %s", m.String(), txt.Quote(title))
if loc.NoCity() || loc.LongCity() || loc.CityContains(title) {
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
} else {
@ -105,7 +105,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
knownLocation = true
if names != "" {
log.Debugf("photo %s: generating title from %s (%s)", txt.Quote(m.PhotoUID), english.Plural(len(people), "person", "people"), txt.Quote(names))
log.Debugf("photo: %s generating title from %s (%s)", m.String(), english.Plural(len(people), "person", "people"), txt.Quote(names))
if l := len([]rune(names)); l > 35 {
m.SetTitle(names, SrcAuto)
@ -119,7 +119,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
m.SetTitle(fmt.Sprintf("%s / %s / %s", names, m.Place.City(), m.TakenAt.Format("2006")), SrcAuto)
}
} else if title := labels.Title(fileTitle); title != "" {
log.Debugf("photo %s: generating title from label %s", txt.Quote(m.PhotoUID), txt.Quote(title))
log.Debugf("photo: %s generating title from label %s", m.String(), 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 {
@ -161,7 +161,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
}
if m.PhotoTitle != oldTitle {
log.Debugf("photo %s: changed title to %s [%s]", txt.Quote(m.PhotoUID), txt.Quote(m.PhotoTitle), time.Since(start))
log.Debugf("photo: %s has new title %s [%s]", m.String(), txt.Quote(m.PhotoTitle), time.Since(start))
}
return nil

View file

@ -74,7 +74,7 @@ func (w *Moments) Start() (err error) {
threshold = int(math.Log2(float64(indexSize))) + 1
}
log.Debugf("moments: analyzing %d photos / %d videos, using threshold %d", counts.Photos, counts.Videos, threshold)
log.Debugf("moments: analyzing %d photos and %d videos, with threshold %d", counts.Photos, counts.Videos, threshold)
if indexSize < threshold {
log.Debugf("moments: not enough files")

View file

@ -93,7 +93,7 @@ func (m *Meta) Start(delay, interval time.Duration, force bool) (err error) {
log.Errorf("metadata: %s (optimize photo)", err)
} else if updated {
optimized++
log.Debugf("metadata: optimized photo %s", photo.String())
log.Debugf("metadata: updated photo %s", photo.String())
}
for _, p := range merged {
@ -112,7 +112,7 @@ func (m *Meta) Start(delay, interval time.Duration, force bool) (err error) {
}
if optimized > 0 {
log.Infof("metadata: optimized %d photos", optimized)
log.Infof("metadata: updated %d photos", optimized)
}
// Set photo quality scores to -1 if files are missing.