Backend: Update photo title when location or labels change
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
61ebd1ac90
commit
e3f614bc23
|
@ -218,7 +218,7 @@ class Photo extends RestModel {
|
|||
}
|
||||
|
||||
addLabel(name) {
|
||||
return Api.post(this.getEntityResource() + "/label", {LabelName: name})
|
||||
return Api.post(this.getEntityResource() + "/label", {LabelName: name, LabelPriority: 10})
|
||||
.then((response) => Promise.resolve(this.setValues(response.data)));
|
||||
}
|
||||
|
||||
|
|
|
@ -18,4 +18,5 @@ var (
|
|||
ErrPhotoNotFound = gin.H{"code": http.StatusNotFound, "error": "Photo not found"}
|
||||
ErrLabelNotFound = gin.H{"code": http.StatusNotFound, "error": "Label not found"}
|
||||
ErrUnexpectedError = gin.H{"code": http.StatusInternalServerError, "error": "Unexpected error"}
|
||||
ErrSaveFailed = gin.H{"code": http.StatusInternalServerError, "error": "Save failed - database error?"}
|
||||
)
|
||||
|
|
|
@ -46,10 +46,10 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
id := c.Param("uuid")
|
||||
uuid := c.Param("uuid")
|
||||
q := query.New(conf.Db())
|
||||
|
||||
m, err := q.PhotoByUUID(id)
|
||||
m, err := q.PhotoByUUID(uuid)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
|
@ -72,16 +72,16 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
|
|||
}
|
||||
|
||||
// 3) Save model with values from form
|
||||
if err := entity.SavePhoto(m, f, conf.Db()); err != nil {
|
||||
if err := entity.SavePhotoForm(m, f, conf.Db(), conf.GeoCodingApi()); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, id, c, q)
|
||||
PublishPhotoEvent(EntityUpdated, uuid, c, q)
|
||||
|
||||
event.Success("photo saved")
|
||||
|
||||
p, err := q.PreloadPhotoByUUID(id)
|
||||
p, err := q.PreloadPhotoByUUID(uuid)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
|
|
|
@ -68,6 +68,11 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := p.Save(db); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, p)
|
||||
})
|
||||
}
|
||||
|
@ -109,6 +114,11 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := p.Save(db); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, p)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ func (l Labels) AppendLabel(label Label) Labels {
|
|||
return append(l, label)
|
||||
}
|
||||
|
||||
// Keywords returns all keywords contains in Labels and theire categories
|
||||
// Keywords returns all keywords contains in Labels and their categories
|
||||
func (l Labels) Keywords() (result []string) {
|
||||
for _, label := range l {
|
||||
result = append(result, txt.Keywords(label.Name)...)
|
||||
|
|
|
@ -14,13 +14,11 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
tf "github.com/tensorflow/tensorflow/tensorflow/go"
|
||||
)
|
||||
|
||||
// TensorFlow is a wrapper for tensorflow low-level API.
|
||||
type TensorFlow struct {
|
||||
conf *config.Config
|
||||
model *tf.SavedModel
|
||||
modelsPath string
|
||||
disabled bool
|
||||
|
|
|
@ -6,17 +6,18 @@ import (
|
|||
|
||||
tensorflow "github.com/tensorflow/tensorflow/tensorflow/go"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var resourcesPath = "../../assets/resources"
|
||||
var modelPath = resourcesPath + "/nasnet"
|
||||
var examplesPath = resourcesPath + "/examples"
|
||||
|
||||
func TestTensorFlow_LabelsFromFile(t *testing.T) {
|
||||
t.Run("chameleon_lime.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
|
||||
result, err := tensorFlow.File(conf.ExamplesPath() + "/chameleon_lime.jpg")
|
||||
result, err := tensorFlow.File(examplesPath + "/chameleon_lime.jpg")
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
|
@ -36,11 +37,9 @@ func TestTensorFlow_LabelsFromFile(t *testing.T) {
|
|||
assert.Equal(t, 7, result[0].Uncertainty)
|
||||
})
|
||||
t.Run("not existing file", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
|
||||
result, err := tensorFlow.File(conf.ExamplesPath() + "/notexisting.jpg")
|
||||
result, err := tensorFlow.File(examplesPath + "/notexisting.jpg")
|
||||
assert.Contains(t, err.Error(), "no such file or directory")
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
@ -52,11 +51,9 @@ func TestTensorFlow_Labels(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run("chameleon_lime.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
|
||||
if imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/chameleon_lime.jpg"); err != nil {
|
||||
if imageBuffer, err := ioutil.ReadFile(examplesPath + "/chameleon_lime.jpg"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
result, err := tensorFlow.Labels(imageBuffer)
|
||||
|
@ -75,11 +72,9 @@ func TestTensorFlow_Labels(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("dog_orange.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
|
||||
if imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/dog_orange.jpg"); err != nil {
|
||||
if imageBuffer, err := ioutil.ReadFile(examplesPath + "/dog_orange.jpg"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
result, err := tensorFlow.Labels(imageBuffer)
|
||||
|
@ -98,11 +93,9 @@ func TestTensorFlow_Labels(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("Random.docx", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
|
||||
if imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/Random.docx"); err != nil {
|
||||
if imageBuffer, err := ioutil.ReadFile(examplesPath + "/Random.docx"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
result, err := tensorFlow.Labels(imageBuffer)
|
||||
|
@ -111,11 +104,9 @@ func TestTensorFlow_Labels(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("6720px_white.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
|
||||
if imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/6720px_white.jpg"); err != nil {
|
||||
if imageBuffer, err := ioutil.ReadFile(examplesPath + "/6720px_white.jpg"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
result, err := tensorFlow.Labels(imageBuffer)
|
||||
|
@ -127,28 +118,27 @@ func TestTensorFlow_Labels(t *testing.T) {
|
|||
|
||||
func TestTensorFlow_LoadModel(t *testing.T) {
|
||||
t.Run("model path exists", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
|
||||
result := tensorFlow.loadModel()
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
t.Run("model path does not exist", func(t *testing.T) {
|
||||
conf := config.NewTestErrorConfig()
|
||||
tensorFlow := New(resourcesPath + "foo", false)
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
err := tensorFlow.loadModel()
|
||||
|
||||
result := tensorFlow.loadModel()
|
||||
assert.Contains(t, result.Error(), "Could not find SavedModel")
|
||||
if err == nil {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.Contains(t, err.Error(), "Could not find SavedModel")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTensorFlow_BestLabels(t *testing.T) {
|
||||
t.Run("labels not loaded", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
|
||||
p := make([]float32, 1000)
|
||||
|
||||
|
@ -158,10 +148,8 @@ func TestTensorFlow_BestLabels(t *testing.T) {
|
|||
assert.Empty(t, result)
|
||||
})
|
||||
t.Run("labels loaded", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
path := conf.TensorFlowModelPath()
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
tensorFlow.loadLabels(path)
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
tensorFlow.loadLabels(modelPath)
|
||||
|
||||
p := make([]float32, 1000)
|
||||
|
||||
|
@ -181,11 +169,9 @@ func TestTensorFlow_BestLabels(t *testing.T) {
|
|||
|
||||
func TestTensorFlow_MakeTensor(t *testing.T) {
|
||||
t.Run("cat_brown.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
|
||||
imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/cat_brown.jpg")
|
||||
imageBuffer, err := ioutil.ReadFile(examplesPath + "/cat_brown.jpg")
|
||||
assert.Nil(t, err)
|
||||
result, err := tensorFlow.makeTensor(imageBuffer, "jpeg")
|
||||
assert.Equal(t, tensorflow.DataType(0x1), result.DataType())
|
||||
|
@ -193,11 +179,9 @@ func TestTensorFlow_MakeTensor(t *testing.T) {
|
|||
assert.Equal(t, int64(224), result.Shape()[2])
|
||||
})
|
||||
t.Run("Random.docx", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
tensorFlow := New(resourcesPath, false)
|
||||
|
||||
tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow())
|
||||
|
||||
imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/Random.docx")
|
||||
imageBuffer, err := ioutil.ReadFile(examplesPath + "/Random.docx")
|
||||
assert.Nil(t, err)
|
||||
result, err := tensorFlow.makeTensor(imageBuffer, "jpeg")
|
||||
assert.Empty(t, result)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
@ -49,7 +50,7 @@ type Photo struct {
|
|||
Description Description `json:"Description"`
|
||||
Camera *Camera `json:"Camera"`
|
||||
Lens *Lens `json:"Lens"`
|
||||
Location *Location `json:"-"`
|
||||
Location *Location `json:"Location"`
|
||||
Place *Place `json:"-"`
|
||||
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:PhotoUUID"`
|
||||
Keywords []Keyword `json:"-"`
|
||||
|
@ -61,8 +62,10 @@ type Photo struct {
|
|||
DeletedAt *time.Time `sql:"index"`
|
||||
}
|
||||
|
||||
// SavePhoto updates a model using form data and persists it in the database.
|
||||
func SavePhoto(model Photo, form form.Photo, db *gorm.DB) error {
|
||||
// SavePhotoForm updates a model using form data and persists it in the database.
|
||||
func SavePhotoForm(model Photo, form form.Photo, db *gorm.DB, geoApi string) error {
|
||||
locChanged := model.PhotoLat != form.PhotoLat || model.PhotoLng != form.PhotoLng
|
||||
|
||||
if err := deepcopier.Copy(&model).From(form); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -75,11 +78,53 @@ func SavePhoto(model Photo, form form.Photo, db *gorm.DB) error {
|
|||
model.Description.PhotoKeywords = strings.Join(txt.UniqueKeywords(model.Description.PhotoKeywords), ", ")
|
||||
}
|
||||
|
||||
if model.HasLatLng() && locChanged && model.ModifiedLocation {
|
||||
w := txt.UniqueKeywords(model.Description.PhotoKeywords)
|
||||
|
||||
var locKeywords []string
|
||||
labels := model.ClassifyLabels()
|
||||
|
||||
locKeywords, labels = model.IndexLocation(db, geoApi, labels)
|
||||
|
||||
w = append(w, locKeywords...)
|
||||
w = append(w, labels.Keywords()...)
|
||||
|
||||
model.Description.PhotoKeywords = strings.Join(txt.UniqueWords(w), ", ")
|
||||
}
|
||||
|
||||
model.IndexKeywords(db)
|
||||
|
||||
return db.Unscoped().Save(&model).Error
|
||||
}
|
||||
|
||||
// ClassifyLabels returns all associated labels as classify.Labels
|
||||
func (m *Photo) ClassifyLabels() classify.Labels {
|
||||
result := classify.Labels{}
|
||||
|
||||
for _, l := range m.Labels {
|
||||
result = append(result, l.ClassifyLabel())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Save stored the entity in the database.
|
||||
func (m *Photo) Save(db *gorm.DB) error {
|
||||
labels := m.ClassifyLabels()
|
||||
|
||||
if err := m.UpdateTitle(labels); err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
|
||||
if m.Description.PhotoID == m.ID {
|
||||
w := txt.UniqueKeywords(m.Description.PhotoKeywords)
|
||||
w = append(w, labels.Keywords()...)
|
||||
m.Description.PhotoKeywords = strings.Join(txt.UniqueWords(w), ", ")
|
||||
}
|
||||
|
||||
return db.Unscoped().Save(m).Error
|
||||
}
|
||||
|
||||
// BeforeCreate computes a unique UUID, and set a default takenAt before indexing a new photo
|
||||
func (m *Photo) BeforeCreate(scope *gorm.Scope) error {
|
||||
if err := scope.SetColumn("PhotoUUID", rnd.PPID('p')); err != nil {
|
||||
|
|
|
@ -2,6 +2,7 @@ package entity
|
|||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
)
|
||||
|
||||
|
@ -44,3 +45,19 @@ func (m *PhotoLabel) FirstOrCreate(db *gorm.DB) *PhotoLabel {
|
|||
|
||||
return m
|
||||
}
|
||||
|
||||
// ClassifyLabel returns the label as classify.Label
|
||||
func (m *PhotoLabel) ClassifyLabel() classify.Label {
|
||||
if m.Label == nil {
|
||||
panic("photo label: label is nil")
|
||||
}
|
||||
|
||||
result := classify.Label{
|
||||
Name: m.Label.LabelName,
|
||||
Source: m.LabelSource,
|
||||
Uncertainty: m.LabelUncertainty,
|
||||
Priority: m.Label.LabelPriority,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
121
internal/entity/photo_location.go
Normal file
121
internal/entity/photo_location.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// IndexLocation updates location and labels based on latitude and longitude.
|
||||
func (m *Photo) IndexLocation(db *gorm.DB, geoApi string, labels classify.Labels) ([]string, classify.Labels) {
|
||||
var location = NewLocation(m.PhotoLat, m.PhotoLng)
|
||||
|
||||
location.Lock()
|
||||
defer location.Unlock()
|
||||
|
||||
var keywords []string
|
||||
|
||||
err := location.Find(db, geoApi)
|
||||
|
||||
if err == nil {
|
||||
if location.Place.New {
|
||||
event.Publish("count.places", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
}
|
||||
|
||||
m.Location = location
|
||||
m.LocationID = location.ID
|
||||
m.Place = location.Place
|
||||
m.PlaceID = location.PlaceID
|
||||
m.LocationEstimated = false
|
||||
|
||||
country := NewCountry(location.CountryCode(), location.CountryName()).FirstOrCreate(db)
|
||||
|
||||
if country.New {
|
||||
event.Publish("count.countries", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
}
|
||||
|
||||
locCategory := location.Category()
|
||||
keywords = append(keywords, location.Keywords()...)
|
||||
|
||||
// Append category from reverse location lookup
|
||||
if locCategory != "" {
|
||||
labels = append(labels, classify.LocationLabel(locCategory, 0, -1))
|
||||
}
|
||||
|
||||
if err := m.UpdateTitle(labels); err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
} else {
|
||||
log.Warn(err)
|
||||
|
||||
m.Place = UnknownPlace
|
||||
m.PlaceID = UnknownPlace.ID
|
||||
}
|
||||
|
||||
if m.Place != nil && (!m.ModifiedLocation || m.PhotoCountry == "" || m.PhotoCountry == "zz") {
|
||||
m.PhotoCountry = m.Place.LocCountry
|
||||
}
|
||||
|
||||
return keywords, labels
|
||||
}
|
||||
|
||||
// UpdateTitle updated the photo title based on location and labels.
|
||||
func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
||||
if m.ModifiedTitle && m.HasTitle() {
|
||||
return errors.New("photo: won't update title, was modified")
|
||||
}
|
||||
|
||||
hasLocation := m.Location != nil && m.Location.Place != nil
|
||||
|
||||
if hasLocation {
|
||||
loc := m.Location
|
||||
|
||||
if title := labels.Title(loc.Name()); title != "" { // TODO: User defined title format
|
||||
log.Infof("photo: using label \"%s\" to create photo title", title)
|
||||
if loc.NoCity() || loc.LongCity() || loc.CityContains(title) {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.CountryName(), m.TakenAt.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.City(), m.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if loc.Name() != "" && loc.City() != "" {
|
||||
if len(loc.Name()) > 45 {
|
||||
m.PhotoTitle = txt.Title(loc.Name())
|
||||
} else if len(loc.Name()) > 20 || len(loc.City()) > 16 || strings.Contains(loc.Name(), loc.City()) {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s", loc.Name(), m.TakenAt.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", loc.Name(), loc.City(), m.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if loc.City() != "" && loc.CountryName() != "" {
|
||||
if len(loc.City()) > 20 {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s", loc.City(), m.TakenAt.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", loc.City(), loc.CountryName(), m.TakenAt.Format("2006"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasLocation || m.NoTitle() {
|
||||
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s", txt.Title(labels[0].Name), m.TakenAt.Format("2006"))
|
||||
} else if !m.TakenAtLocal.IsZero() {
|
||||
m.PhotoTitle = fmt.Sprintf("Unknown / %s", m.TakenAtLocal.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = "Unknown"
|
||||
}
|
||||
|
||||
log.Infof("photo: changed empty photo title to \"%s\"", m.PhotoTitle)
|
||||
} else {
|
||||
log.Infof("photo: new title is \"%s\"", m.PhotoTitle)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -92,36 +92,36 @@ func (m *Place) FirstOrCreate(db *gorm.DB) *Place {
|
|||
}
|
||||
|
||||
// NoID checks is the place has no id
|
||||
func (m *Place) NoID() bool {
|
||||
func (m Place) NoID() bool {
|
||||
return m.ID == ""
|
||||
}
|
||||
|
||||
// Label returns place label
|
||||
func (m *Place) Label() string {
|
||||
func (m Place) Label() string {
|
||||
return m.LocLabel
|
||||
}
|
||||
|
||||
// City returns place City
|
||||
func (m *Place) City() string {
|
||||
func (m Place) City() string {
|
||||
return m.LocCity
|
||||
}
|
||||
|
||||
// State returns place State
|
||||
func (m *Place) State() string {
|
||||
func (m Place) State() string {
|
||||
return m.LocState
|
||||
}
|
||||
|
||||
// CountryCode returns place CountryCode
|
||||
func (m *Place) CountryCode() string {
|
||||
func (m Place) CountryCode() string {
|
||||
return m.LocCountry
|
||||
}
|
||||
|
||||
// CountryName returns place CountryName
|
||||
func (m *Place) CountryName() string {
|
||||
func (m Place) CountryName() string {
|
||||
return maps.CountryNames[m.LocCountry]
|
||||
}
|
||||
|
||||
// Notes returns place Notes
|
||||
func (m *Place) Notes() string {
|
||||
func (m Place) Notes() string {
|
||||
return m.LocNotes
|
||||
}
|
||||
|
|
|
@ -1,98 +1,10 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
func IndexLocation(db *gorm.DB, conf *config.Config, location *entity.Location, photo *entity.Photo, labels classify.Labels, fileChanged bool, o IndexOptions) ([]string, classify.Labels) {
|
||||
location.Lock()
|
||||
defer location.Unlock()
|
||||
|
||||
var keywords []string
|
||||
|
||||
err := location.Find(db, conf.GeoCodingApi())
|
||||
|
||||
if err == nil {
|
||||
if location.Place.New {
|
||||
event.Publish("count.places", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
}
|
||||
|
||||
photo.Location = location
|
||||
photo.LocationID = location.ID
|
||||
photo.Place = location.Place
|
||||
photo.PlaceID = location.PlaceID
|
||||
photo.LocationEstimated = false
|
||||
|
||||
country := entity.NewCountry(location.CountryCode(), location.CountryName()).FirstOrCreate(db)
|
||||
|
||||
if country.New {
|
||||
event.Publish("count.countries", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
}
|
||||
|
||||
locCategory := location.Category()
|
||||
keywords = append(keywords, location.Keywords()...)
|
||||
|
||||
// Append category from reverse location lookup
|
||||
if locCategory != "" {
|
||||
labels = append(labels, classify.LocationLabel(locCategory, 0, -1))
|
||||
}
|
||||
|
||||
if (fileChanged || o.UpdateTitle) && !photo.ModifiedTitle {
|
||||
if title := labels.Title(location.Name()); title != "" { // TODO: User defined title format
|
||||
log.Infof("index: using label \"%s\" to create photo title", title)
|
||||
if location.NoCity() || location.LongCity() || location.CityContains(title) {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", txt.Title(title), location.CountryName(), photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", txt.Title(title), location.City(), photo.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if location.Name() != "" && location.City() != "" {
|
||||
if len(location.Name()) > 45 {
|
||||
photo.PhotoTitle = txt.Title(location.Name())
|
||||
} else if len(location.Name()) > 20 || len(location.City()) > 16 || strings.Contains(location.Name(), location.City()) {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.Name(), photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.Name(), location.City(), photo.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if location.City() != "" && location.CountryName() != "" {
|
||||
if len(location.City()) > 20 {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.City(), photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.City(), location.CountryName(), photo.TakenAt.Format("2006"))
|
||||
}
|
||||
}
|
||||
|
||||
if photo.NoTitle() {
|
||||
log.Warn("index: could not set photo title based on location or labels")
|
||||
} else {
|
||||
log.Infof("index: new photo title is \"%s\"", photo.PhotoTitle)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Warn(err)
|
||||
|
||||
photo.Place = entity.UnknownPlace
|
||||
photo.PlaceID = entity.UnknownPlace.ID
|
||||
}
|
||||
|
||||
if !photo.ModifiedLocation || photo.PhotoCountry == "" || photo.PhotoCountry == "zz" {
|
||||
photo.PhotoCountry = photo.Place.LocCountry
|
||||
}
|
||||
|
||||
return keywords, labels
|
||||
}
|
||||
|
||||
func (ind *Index) estimateLocation(photo *entity.Photo) {
|
||||
var recentPhoto entity.Photo
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package photoprism
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
@ -212,36 +211,25 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
photo.PhotoExposure = m.Exposure()
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateKeywords || o.UpdateLocation || o.UpdateTitle {
|
||||
if photo.TakenAt.IsZero() || photo.TakenAtLocal.IsZero() {
|
||||
photo.TakenAt = m.DateCreated()
|
||||
photo.TakenAtLocal = photo.TakenAt
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateKeywords || o.UpdateLocation || o.UpdateTitle || photo.NoTitle() {
|
||||
if photo.HasLatLng() {
|
||||
var locLabels classify.Labels
|
||||
var location = entity.NewLocation(photo.PhotoLat, photo.PhotoLng)
|
||||
locKeywords, locLabels = IndexLocation(ind.db, ind.conf, location, &photo, labels, fileChanged, o)
|
||||
labels = append(labels, locLabels...)
|
||||
locKeywords, labels = photo.IndexLocation(ind.db, ind.conf.GeoCodingApi(), labels)
|
||||
} else {
|
||||
log.Info("index: no latitude and longitude in metadata")
|
||||
|
||||
if err := photo.UpdateTitle(labels); err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
|
||||
photo.Place = entity.UnknownPlace
|
||||
photo.PlaceID = entity.UnknownPlace.ID
|
||||
}
|
||||
}
|
||||
|
||||
if photo.NoTitle() || (fileChanged || o.UpdateTitle) && !photo.ModifiedTitle && photo.NoLocation() {
|
||||
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", txt.Title(labels[0].Name), m.DateCreated().Format("2006"))
|
||||
} else if !photo.TakenAtLocal.IsZero() {
|
||||
photo.PhotoTitle = fmt.Sprintf("Unknown / %s", photo.TakenAtLocal.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = "Unknown"
|
||||
}
|
||||
|
||||
log.Infof("index: changed empty photo title to \"%s\"", photo.PhotoTitle)
|
||||
}
|
||||
|
||||
if photo.TakenAt.IsZero() || photo.TakenAtLocal.IsZero() {
|
||||
photo.TakenAt = m.DateCreated()
|
||||
photo.TakenAtLocal = photo.TakenAt
|
||||
}
|
||||
} else if m.IsXMP() {
|
||||
// TODO: Proof-of-concept for indexing XMP sidecar files
|
||||
if data, err := meta.XMP(m.FileName()); err == nil {
|
||||
|
@ -267,8 +255,10 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
}
|
||||
}
|
||||
|
||||
photo.PhotoYear = photo.TakenAt.Year()
|
||||
photo.PhotoMonth = int(photo.TakenAt.Month())
|
||||
if !photo.TakenAtLocal.IsZero() {
|
||||
photo.PhotoYear = photo.TakenAtLocal.Year()
|
||||
photo.PhotoMonth = int(photo.TakenAtLocal.Month())
|
||||
}
|
||||
|
||||
if originalName != "" {
|
||||
file.OriginalName = originalName
|
||||
|
|
|
@ -347,7 +347,16 @@ func (q *Query) Photos(f form.PhotoSearch) (results []PhotoResult, err error) {
|
|||
|
||||
// PhotoByID returns a Photo based on the ID.
|
||||
func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) {
|
||||
if err := q.db.Unscoped().Where("id = ?", photoID).Preload("Links").Preload("Description").First(&photo).Error; err != nil {
|
||||
if err := q.db.Unscoped().Where("id = ?", photoID).
|
||||
Preload("Links").
|
||||
Preload("Description").
|
||||
Preload("Location").
|
||||
Preload("Location.Place").
|
||||
Preload("Labels", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("photos_labels.label_uncertainty ASC, photos_labels.label_id DESC")
|
||||
}).
|
||||
Preload("Labels.Label").
|
||||
First(&photo).Error; err != nil {
|
||||
return photo, err
|
||||
}
|
||||
|
||||
|
@ -356,7 +365,16 @@ func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) {
|
|||
|
||||
// PhotoByUUID returns a Photo based on the UUID.
|
||||
func (q *Query) PhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
|
||||
if err := q.db.Unscoped().Where("photo_uuid = ?", photoUUID).Preload("Links").Preload("Description").First(&photo).Error; err != nil {
|
||||
if err := q.db.Unscoped().Where("photo_uuid = ?", photoUUID).
|
||||
Preload("Links").
|
||||
Preload("Description").
|
||||
Preload("Location").
|
||||
Preload("Location.Place").
|
||||
Preload("Labels", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("photos_labels.label_uncertainty ASC, photos_labels.label_id DESC")
|
||||
}).
|
||||
Preload("Labels.Label").
|
||||
First(&photo).Error; err != nil {
|
||||
return photo, err
|
||||
}
|
||||
|
||||
|
@ -373,6 +391,8 @@ func (q *Query) PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err er
|
|||
Preload("Camera").
|
||||
Preload("Lens").
|
||||
Preload("Links").
|
||||
Preload("Location").
|
||||
Preload("Location.Place").
|
||||
Preload("Description").
|
||||
First(&photo).Error; err != nil {
|
||||
return photo, err
|
||||
|
|
Loading…
Reference in a new issue