Backend: Update photo title when location or labels change

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-04-16 20:57:00 +02:00
parent 61ebd1ac90
commit e3f614bc23
14 changed files with 279 additions and 181 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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