Metadata: Remove location labels when photo doesn't have a location
This commit is contained in:
parent
08fe7326e5
commit
ff66b3bccc
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue