People: Generate photo titles from subject names #22
This commit is contained in:
parent
1be409d654
commit
9acd4a25b9
|
@ -103,17 +103,12 @@ func UpdateMarker(router *gin.RouterGroup) {
|
|||
|
||||
// Update photo metadata.
|
||||
if p, err := query.PhotoByUID(file.PhotoUID); err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
log.Errorf("faces: %s (find photo))", err)
|
||||
} else if err := p.UpdateAndSaveTitle(); err != nil {
|
||||
log.Errorf("faces: %s (update photo title)", err)
|
||||
} else {
|
||||
if faceCount := file.FaceCount(); p.PhotoFaces == faceCount {
|
||||
// Do nothing.
|
||||
} else if err := p.Update("PhotoFaces", faceCount); err != nil {
|
||||
log.Errorf("photo: %s (update face count)", err)
|
||||
} else {
|
||||
// Notify clients.
|
||||
PublishPhotoEvent(EntityUpdated, file.PhotoUID, c)
|
||||
}
|
||||
// Notify clients.
|
||||
PublishPhotoEvent(EntityUpdated, file.PhotoUID, c)
|
||||
}
|
||||
|
||||
event.SuccessMsg(i18n.MsgChangesSaved)
|
||||
|
@ -132,7 +127,7 @@ func UpdateMarker(router *gin.RouterGroup) {
|
|||
// id: int Marker ID as returned by the API
|
||||
func ClearMarkerSubject(router *gin.RouterGroup) {
|
||||
router.DELETE("/markers/:marker_uid/subject", func(c *gin.Context) {
|
||||
_, marker, err := findFileMarker(c)
|
||||
file, marker, err := findFileMarker(c)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("api: %s (clear marker subject)", err)
|
||||
|
@ -145,6 +140,16 @@ func ClearMarkerSubject(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
// Update photo metadata.
|
||||
if p, err := query.PhotoByUID(file.PhotoUID); err != nil {
|
||||
log.Errorf("faces: %s (find photo))", err)
|
||||
} else if err := p.UpdateAndSaveTitle(); err != nil {
|
||||
log.Errorf("faces: %s (update photo title)", err)
|
||||
} else {
|
||||
// Notify clients.
|
||||
PublishPhotoEvent(EntityUpdated, file.PhotoUID, c)
|
||||
}
|
||||
|
||||
event.SuccessMsg(i18n.MsgChangesSaved)
|
||||
|
||||
c.JSON(http.StatusOK, marker)
|
||||
|
|
|
@ -94,12 +94,12 @@ func FirstFileByHash(fileHash string) (File, error) {
|
|||
}
|
||||
|
||||
// PrimaryFile returns the primary file for a photo uid.
|
||||
func PrimaryFile(photoUID string) (File, error) {
|
||||
var file File
|
||||
func PrimaryFile(photoUID string) (*File, error) {
|
||||
file := File{}
|
||||
|
||||
res := Db().Unscoped().First(&file, "file_primary = 1 AND photo_uid = ?", photoUID)
|
||||
|
||||
return file, res.Error
|
||||
return &file, res.Error
|
||||
}
|
||||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
|
|
|
@ -463,16 +463,21 @@ func TestFile_FaceCount(t *testing.T) {
|
|||
func TestFile_Rename(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := FileFixtures.Get("exampleFileName.jpg")
|
||||
|
||||
assert.Equal(t, "2790/07/27900704_070228_D6D51B6C.jpg", m.FileName)
|
||||
assert.Equal(t, RootOriginals, m.FileRoot)
|
||||
assert.Equal(t, false, m.FileMissing)
|
||||
assert.Nil(t, m.DeletedAt)
|
||||
|
||||
p := m.RelatedPhoto()
|
||||
|
||||
assert.Equal(t, "2790/07", p.PhotoPath)
|
||||
assert.Equal(t, "27900704_070228_D6D51B6C", p.PhotoName)
|
||||
|
||||
m.Rename("x/y/newName.jpg", "newRoot", "x/y", "newBase")
|
||||
if err := m.Rename("x/y/newName.jpg", "newRoot", "x/y", "newBase"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "x/y/newName.jpg", m.FileName)
|
||||
assert.Equal(t, "newRoot", m.FileRoot)
|
||||
assert.Equal(t, false, m.FileMissing)
|
||||
|
@ -480,7 +485,10 @@ func TestFile_Rename(t *testing.T) {
|
|||
assert.Equal(t, "x/y", p.PhotoPath)
|
||||
assert.Equal(t, "newBase", p.PhotoName)
|
||||
|
||||
m.Rename("2790/07/27900704_070228_D6D51B6C.jpg", RootOriginals, "2790/07", "27900704_070228_D6D51B6C")
|
||||
if err := m.Rename("2790/07/27900704_070228_D6D51B6C.jpg", RootOriginals, "2790/07", "27900704_070228_D6D51B6C"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "2790/07/27900704_070228_D6D51B6C.jpg", m.FileName)
|
||||
assert.Equal(t, RootOriginals, m.FileRoot)
|
||||
assert.Equal(t, false, m.FileMissing)
|
||||
|
@ -491,13 +499,37 @@ func TestFile_Rename(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFile_SubjectNames(t *testing.T) {
|
||||
f := FileFixtures.Get("Video.mp4")
|
||||
names := f.SubjectNames()
|
||||
t.Run("Video.jpg", func(t *testing.T) {
|
||||
m := FileFixtures.Get("Video.jpg")
|
||||
|
||||
assert.Len(t, names, 1)
|
||||
if len(names) != 1 {
|
||||
t.Fatal("there should be one name")
|
||||
} else {
|
||||
assert.Equal(t, "Actress A", names[0])
|
||||
}
|
||||
names := m.SubjectNames()
|
||||
|
||||
if len(names) != 1 {
|
||||
t.Errorf("there should be one name: %#v", names)
|
||||
} else {
|
||||
assert.Equal(t, "Actor A", names[0])
|
||||
}
|
||||
})
|
||||
t.Run("Video.mp4", func(t *testing.T) {
|
||||
m := FileFixtures.Get("Video.mp4")
|
||||
|
||||
names := m.SubjectNames()
|
||||
|
||||
if len(names) != 1 {
|
||||
t.Errorf("there should be one name: %#v", names)
|
||||
} else {
|
||||
assert.Equal(t, "Actress A", names[0])
|
||||
}
|
||||
})
|
||||
t.Run("bridge.jpg", func(t *testing.T) {
|
||||
m := FileFixtures.Get("bridge.jpg")
|
||||
|
||||
names := m.SubjectNames()
|
||||
|
||||
if len(names) != 2 {
|
||||
t.Errorf("two names expected: %#v", names)
|
||||
} else {
|
||||
assert.Equal(t, []string{"Corn McCornface", "Jens Mander"}, names)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package entity
|
||||
|
||||
import "github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
type Markers []Marker
|
||||
|
||||
// Save stores the markers in the database.
|
||||
|
@ -51,7 +53,7 @@ func (m Markers) SubjectNames() (names []string) {
|
|||
}
|
||||
}
|
||||
|
||||
return names
|
||||
return txt.UniqueNames(names)
|
||||
}
|
||||
|
||||
// Append adds a marker.
|
||||
|
|
|
@ -27,3 +27,15 @@ func TestMarkers_FaceCount(t *testing.T) {
|
|||
|
||||
assert.Equal(t, 2, m.FaceCount())
|
||||
}
|
||||
|
||||
func TestMarkers_SubjectNames(t *testing.T) {
|
||||
m1 := MarkerFixtures.Get("1000003-3")
|
||||
m2 := MarkerFixtures.Get("1000003-4")
|
||||
m3 := MarkerFixtures.Get("1000003-5")
|
||||
|
||||
m1.MarkerInvalid = true
|
||||
|
||||
m := Markers{m1, m2, m3}
|
||||
|
||||
assert.Equal(t, []string{"Jens Mander", "Corn McCornface"}, m.SubjectNames())
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
"github.com/ulule/deepcopier"
|
||||
|
@ -284,7 +283,7 @@ func (m *Photo) Find() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Save the photo to the database.
|
||||
// SaveLabels updates the photo after labels have changed.
|
||||
func (m *Photo) SaveLabels() error {
|
||||
if !m.HasID() {
|
||||
return errors.New("photo: can't save to database, id is empty")
|
||||
|
@ -602,11 +601,6 @@ func (m *Photo) UnknownCountry() bool {
|
|||
return m.CountryCode() == UnknownCountry.ID
|
||||
}
|
||||
|
||||
// NoTitle checks if the photo has no Title
|
||||
func (m *Photo) NoTitle() bool {
|
||||
return m.PhotoTitle == ""
|
||||
}
|
||||
|
||||
// NoCameraSerial checks if the photo has no CameraSerial
|
||||
func (m *Photo) NoCameraSerial() bool {
|
||||
return m.CameraSerial == ""
|
||||
|
@ -622,11 +616,6 @@ func (m *Photo) UnknownLens() bool {
|
|||
return m.LensID == 0 || m.LensID == UnknownLens.ID
|
||||
}
|
||||
|
||||
// HasTitle checks if the photo has a title.
|
||||
func (m *Photo) HasTitle() bool {
|
||||
return m.PhotoTitle != ""
|
||||
}
|
||||
|
||||
// HasDescription checks if the photo has a description.
|
||||
func (m *Photo) HasDescription() bool {
|
||||
return m.PhotoDescription != ""
|
||||
|
@ -664,116 +653,6 @@ func (m *Photo) SaveDetails() error {
|
|||
}
|
||||
}
|
||||
|
||||
// FileTitle returns a photo title based on the file name and/or path.
|
||||
func (m *Photo) FileTitle() string {
|
||||
// Generate title based on photo name, if not generated:
|
||||
if !fs.IsGenerated(m.PhotoName) {
|
||||
if title := txt.FileTitle(m.PhotoName); title != "" {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
// Generate title based on original file name, if any:
|
||||
if m.OriginalName != "" {
|
||||
if title := txt.FileTitle(m.OriginalName); !fs.IsGenerated(m.OriginalName) && title != "" {
|
||||
return title
|
||||
} else if title := txt.FileTitle(filepath.Dir(m.OriginalName)); title != "" {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
// Generate title based on photo path, if any:
|
||||
if m.PhotoPath != "" && !fs.IsGenerated(m.PhotoPath) {
|
||||
return txt.FileTitle(m.PhotoPath)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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: won't update title, %s was modified", m.PhotoUID)
|
||||
}
|
||||
|
||||
var knownLocation bool
|
||||
|
||||
oldTitle := m.PhotoTitle
|
||||
fileTitle := m.FileTitle()
|
||||
|
||||
if m.LocationLoaded() {
|
||||
knownLocation = true
|
||||
loc := m.Cell
|
||||
|
||||
// TODO: User defined title format
|
||||
if title := labels.Title(loc.Name()); title != "" {
|
||||
log.Debugf("photo: using label %s to create title for %s", txt.Quote(title), m.PhotoUID)
|
||||
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 {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.City(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
}
|
||||
} else if loc.Name() != "" && loc.City() != "" {
|
||||
if len(loc.Name()) > 45 {
|
||||
m.SetTitle(txt.Title(loc.Name()), SrcAuto)
|
||||
} else if len(loc.Name()) > 20 || len(loc.City()) > 16 || strings.Contains(loc.Name(), loc.City()) {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", loc.Name(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", loc.Name(), loc.City(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
}
|
||||
} else if loc.City() != "" && loc.CountryName() != "" {
|
||||
if len(loc.City()) > 20 {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", loc.City(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
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(fileTitle); title != "" {
|
||||
log.Debugf("photo: using label %s to create title for %s", txt.Quote(title), m.PhotoUID)
|
||||
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() {
|
||||
if fileTitle == "" && len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
|
||||
if m.TakenSrc != SrcAuto {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", txt.Title(labels[0].Name), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
m.SetTitle(txt.Title(labels[0].Name), SrcAuto)
|
||||
}
|
||||
} else if fileTitle != "" && len(fileTitle) <= 20 && !m.TakenAtLocal.IsZero() && m.TakenSrc != SrcAuto {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", fileTitle, m.TakenAtLocal.Format("2006")), SrcAuto)
|
||||
} else if fileTitle != "" {
|
||||
m.SetTitle(fileTitle, SrcAuto)
|
||||
} else {
|
||||
if m.TakenSrc != SrcAuto {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", UnknownTitle, m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
m.SetTitle(UnknownTitle, SrcAuto)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.PhotoTitle != oldTitle {
|
||||
log.Debugf("photo: changed title of %s to %s", m.PhotoUID, txt.Quote(m.PhotoTitle))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddLabels updates the entity with additional or updated label information.
|
||||
func (m *Photo) AddLabels(labels classify.Labels) {
|
||||
for _, classifyLabel := range labels {
|
||||
|
@ -813,22 +692,6 @@ func (m *Photo) AddLabels(labels classify.Labels) {
|
|||
Db().Set("gorm:auto_preload", true).Model(m).Related(&m.Labels)
|
||||
}
|
||||
|
||||
// SetTitle changes the photo title and clips it to 300 characters.
|
||||
func (m *Photo) SetTitle(title, source string) {
|
||||
newTitle := txt.Clip(title, txt.ClipDefault)
|
||||
|
||||
if newTitle == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if (SrcPriority[source] < SrcPriority[m.TitleSrc]) && m.HasTitle() {
|
||||
return
|
||||
}
|
||||
|
||||
m.PhotoTitle = newTitle
|
||||
m.TitleSrc = source
|
||||
}
|
||||
|
||||
// SetDescription changes the photo description if not empty and from the same source.
|
||||
func (m *Photo) SetDescription(desc, source string) {
|
||||
newDesc := txt.Clip(desc, txt.ClipDescription)
|
||||
|
@ -1159,7 +1022,7 @@ func (m *Photo) Links() Links {
|
|||
}
|
||||
|
||||
// PrimaryFile returns the primary file for this photo.
|
||||
func (m *Photo) PrimaryFile() (File, error) {
|
||||
func (m *Photo) PrimaryFile() (*File, error) {
|
||||
return PrimaryFile(m.PhotoUID)
|
||||
}
|
||||
|
||||
|
@ -1211,3 +1074,12 @@ func (m *Photo) SetCameraSerial(s string) {
|
|||
m.CameraSerial = val
|
||||
}
|
||||
}
|
||||
|
||||
// FaceCount returns the current number of faces on the primary picture.
|
||||
func (m *Photo) FaceCount() int {
|
||||
if f, err := m.PrimaryFile(); err != nil {
|
||||
return 0
|
||||
} else {
|
||||
return f.FaceCount()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -261,28 +261,6 @@ func TestPhoto_HasPlace(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPhoto_HasTitle(t *testing.T) {
|
||||
t.Run("false", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo03")
|
||||
assert.False(t, m.HasTitle())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo04")
|
||||
assert.True(t, m.HasTitle())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_NoTitle(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo03")
|
||||
assert.True(t, m.NoTitle())
|
||||
})
|
||||
t.Run("false", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo04")
|
||||
assert.False(t, m.NoTitle())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_NoCameraSerial(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo04")
|
||||
|
@ -331,170 +309,6 @@ func TestPhoto_GetDetails(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPhoto_FileTitle(t *testing.T) {
|
||||
t.Run("non-latin", func(t *testing.T) {
|
||||
photo := Photo{PhotoName: "桥", PhotoPath: "", OriginalName: ""}
|
||||
result := photo.FileTitle()
|
||||
assert.Equal(t, "桥", result)
|
||||
})
|
||||
t.Run("changing-of-the-guard--buckingham-palace_7925318070_o.jpg", func(t *testing.T) {
|
||||
photo := Photo{PhotoName: "20200102_194030_9EFA9E5E", PhotoPath: "2000/05", OriginalName: "flickr import/changing-of-the-guard--buckingham-palace_7925318070_o.jpg"}
|
||||
result := photo.FileTitle()
|
||||
assert.Equal(t, "Changing of the Guard / Buckingham Palace", result)
|
||||
})
|
||||
t.Run("empty title", func(t *testing.T) {
|
||||
photo := Photo{PhotoName: "", PhotoPath: "", OriginalName: ""}
|
||||
result := photo.FileTitle()
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
t.Run("return title", func(t *testing.T) {
|
||||
photo := Photo{PhotoName: "sun, beach, fun", PhotoPath: "", OriginalName: "", PhotoTitle: ""}
|
||||
result := photo.FileTitle()
|
||||
assert.Equal(t, "Sun, Beach, Fun", result)
|
||||
})
|
||||
t.Run("return title", func(t *testing.T) {
|
||||
photo := Photo{PhotoName: "", PhotoPath: "vacation", OriginalName: "20200102_194030_9EFA9E5E", PhotoTitle: ""}
|
||||
result := photo.FileTitle()
|
||||
assert.Equal(t, "Vacation", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_UpdateTitle(t *testing.T) {
|
||||
t.Run("wont update title was modified", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo08")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Black beach", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
assert.Equal(t, "Black beach", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location without city and label", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo10")
|
||||
classifyLabels := &classify.Labels{{Name: "tree", Uncertainty: 30, Source: "manual", Priority: 5, Categories: []string{"plant"}}}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Tree / Germany / 2016", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location and short city and label", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo09")
|
||||
classifyLabels := &classify.Labels{{Name: "tree", Uncertainty: 30, Source: "manual", Priority: 5, Categories: []string{"plant"}}}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Tree / Teotihuacán / 2016", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location and locname >45", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo13")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "LonglonglonglonglonglonglonglonglonglonglonglonglongName", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location and locname >20", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo14")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "longlonglonglonglonglongName / 2016", m.PhotoTitle)
|
||||
})
|
||||
|
||||
t.Run("photo with location and short city", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo09")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Adosada Platform / Teotihuacán / 2016", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location without city", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo10")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Holiday Park / Germany / 2016", m.PhotoTitle)
|
||||
})
|
||||
|
||||
t.Run("photo with location without loc name and long city", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo11")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "longlonglonglonglongcity / 2016", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location without loc name and short city", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo12")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "shortcity / Germany / 2016", m.PhotoTitle)
|
||||
})
|
||||
t.Run("no location original name", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("19800101_000002_D640C559")
|
||||
classifyLabels := &classify.Labels{{Name: "classify", Uncertainty: 30, Source: SrcManual, Priority: 5, Categories: []string{"flower", "plant"}}}
|
||||
assert.Equal(t, "Lake / 2790", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Examplefilenameoriginal", m.PhotoTitle)
|
||||
})
|
||||
t.Run("no location", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo01")
|
||||
classifyLabels := &classify.Labels{{Name: "classify", Uncertainty: 30, Source: SrcManual, Priority: 5, Categories: []string{"flower", "plant"}}}
|
||||
assert.Equal(t, "", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Classify / Germany / 2006", m.PhotoTitle)
|
||||
})
|
||||
|
||||
t.Run("no location no labels", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo02")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Bridge / 1990", m.PhotoTitle)
|
||||
})
|
||||
t.Run("no location no labels no takenAt", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo20")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Unknown", m.PhotoTitle)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_AddLabels(t *testing.T) {
|
||||
t.Run("add label", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("19800101_000002_D640C559")
|
||||
|
@ -516,27 +330,6 @@ func TestPhoto_AddLabels(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPhoto_SetTitle(t *testing.T) {
|
||||
t.Run("empty title", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
|
||||
m.SetTitle("", SrcManual)
|
||||
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
|
||||
})
|
||||
t.Run("title not from the same source", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
|
||||
m.SetTitle("NewTitleSet", SrcAuto)
|
||||
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
|
||||
})
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
|
||||
m.SetTitle("NewTitleSet", SrcName)
|
||||
assert.Equal(t, "NewTitleSet", m.PhotoTitle)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_SetDescription(t *testing.T) {
|
||||
t.Run("empty description", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
|
|
246
internal/entity/photo_title.go
Normal file
246
internal/entity/photo_title.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// HasTitle checks if the photo has a title.
|
||||
func (m *Photo) HasTitle() bool {
|
||||
return m.PhotoTitle != ""
|
||||
}
|
||||
|
||||
// NoTitle checks if the photo has no Title
|
||||
func (m *Photo) NoTitle() bool {
|
||||
return m.PhotoTitle == ""
|
||||
}
|
||||
|
||||
// SetTitle changes the photo title and clips it to 300 characters.
|
||||
func (m *Photo) SetTitle(title, source string) {
|
||||
newTitle := txt.Clip(title, txt.ClipDefault)
|
||||
|
||||
if newTitle == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if (SrcPriority[source] < SrcPriority[m.TitleSrc]) && m.HasTitle() {
|
||||
return
|
||||
}
|
||||
|
||||
m.PhotoTitle = newTitle
|
||||
m.TitleSrc = source
|
||||
}
|
||||
|
||||
// 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: won't update title, %s was modified", m.PhotoUID)
|
||||
}
|
||||
|
||||
var names string
|
||||
var knownLocation bool
|
||||
|
||||
oldTitle := m.PhotoTitle
|
||||
fileTitle := m.FileTitle()
|
||||
|
||||
people := m.SubjectNames()
|
||||
|
||||
m.UpdateDescription(people)
|
||||
|
||||
if n := len(people); n > 0 && n < 4 {
|
||||
names = txt.JoinNames(people)
|
||||
}
|
||||
|
||||
if m.LocationLoaded() {
|
||||
knownLocation = true
|
||||
loc := m.Cell
|
||||
|
||||
// TODO: User defined title format
|
||||
if names != "" {
|
||||
log.Debugf("photo: using %s to create title for %s", txt.Quote(names), m.PhotoUID)
|
||||
|
||||
if l := len([]rune(names)); l > 35 {
|
||||
m.SetTitle(names, SrcAuto)
|
||||
} else if l > 20 && (loc.NoCity() || loc.LongCity()) {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", names, m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else if l > 20 {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", names, loc.City()), SrcAuto)
|
||||
} else if loc.NoCity() || loc.LongCity() {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", names, loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
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: using label %s to create title for %s", txt.Quote(title), m.PhotoUID)
|
||||
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 {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.City(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
}
|
||||
} else if loc.Name() != "" && loc.City() != "" {
|
||||
if len(loc.Name()) > 45 {
|
||||
m.SetTitle(txt.Title(loc.Name()), SrcAuto)
|
||||
} else if len(loc.Name()) > 20 || len(loc.City()) > 16 || strings.Contains(loc.Name(), loc.City()) {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", loc.Name(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", loc.Name(), loc.City(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
}
|
||||
} else if loc.City() != "" && loc.CountryName() != "" {
|
||||
if len(loc.City()) > 20 {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", loc.City(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", loc.City(), loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
}
|
||||
}
|
||||
} else if m.PlaceLoaded() {
|
||||
knownLocation = true
|
||||
|
||||
if names != "" {
|
||||
log.Debugf("photo: using %s to create title for %s", txt.Quote(names), m.PhotoUID)
|
||||
|
||||
if l := len([]rune(names)); l > 35 {
|
||||
m.SetTitle(names, SrcAuto)
|
||||
} else if l > 20 && (m.Place.NoCity() || m.Place.LongCity()) {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", names, m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else if l > 20 {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", names, m.Place.City()), SrcAuto)
|
||||
} else if m.Place.NoCity() || m.Place.LongCity() {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", names, m.Place.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
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: using label %s to create title for %s", txt.Quote(title), m.PhotoUID)
|
||||
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() {
|
||||
if names != "" {
|
||||
if len([]rune(names)) <= 35 && m.TakenSrc != SrcAuto {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", names, m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
m.SetTitle(names, SrcAuto)
|
||||
}
|
||||
} else if fileTitle == "" && len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
|
||||
if m.TakenSrc != SrcAuto {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", txt.Title(labels[0].Name), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
m.SetTitle(txt.Title(labels[0].Name), SrcAuto)
|
||||
}
|
||||
} else if fileTitle != "" && len(fileTitle) <= 20 && !m.TakenAtLocal.IsZero() && m.TakenSrc != SrcAuto {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", fileTitle, m.TakenAtLocal.Format("2006")), SrcAuto)
|
||||
} else if fileTitle != "" {
|
||||
m.SetTitle(fileTitle, SrcAuto)
|
||||
} else {
|
||||
if m.TakenSrc != SrcAuto {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s", UnknownTitle, m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
m.SetTitle(UnknownTitle, SrcAuto)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.PhotoTitle != oldTitle {
|
||||
log.Debugf("photo: changed title of %s to %s", m.PhotoUID, txt.Quote(m.PhotoTitle))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAndSaveTitle updates the photo title and saves it.
|
||||
func (m *Photo) UpdateAndSaveTitle() error {
|
||||
if !m.HasID() {
|
||||
return fmt.Errorf("photo: can't save to database, id is empty")
|
||||
}
|
||||
|
||||
m.PhotoFaces = m.FaceCount()
|
||||
|
||||
labels := m.ClassifyLabels()
|
||||
|
||||
m.UpdateDateFields()
|
||||
|
||||
if err := m.UpdateTitle(labels); err != nil {
|
||||
log.Info(err)
|
||||
}
|
||||
|
||||
details := m.GetDetails()
|
||||
|
||||
w := txt.UniqueWords(txt.Words(details.Keywords))
|
||||
w = append(w, labels.Keywords()...)
|
||||
details.Keywords = strings.Join(txt.UniqueWords(w), ", ")
|
||||
|
||||
if err := m.IndexKeywords(); err != nil {
|
||||
log.Errorf("photo: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := m.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateDescription updates the photo descriptions based on available metadata.
|
||||
func (m *Photo) UpdateDescription(people []string) {
|
||||
if m.DescriptionSrc != SrcAuto {
|
||||
return
|
||||
}
|
||||
|
||||
// Add subject names to description when there's more than one person.
|
||||
if len(people) > 3 {
|
||||
m.PhotoDescription = txt.JoinNames(people)
|
||||
} else {
|
||||
m.PhotoDescription = ""
|
||||
}
|
||||
}
|
||||
|
||||
// FileTitle returns a photo title based on the file name and/or path.
|
||||
func (m *Photo) FileTitle() string {
|
||||
// Generate title based on photo name, if not generated:
|
||||
if !fs.IsGenerated(m.PhotoName) {
|
||||
if title := txt.FileTitle(m.PhotoName); title != "" {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
// Generate title based on original file name, if any:
|
||||
if m.OriginalName != "" {
|
||||
if title := txt.FileTitle(m.OriginalName); !fs.IsGenerated(m.OriginalName) && title != "" {
|
||||
return title
|
||||
} else if title := txt.FileTitle(filepath.Dir(m.OriginalName)); title != "" {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
// Generate title based on photo path, if any:
|
||||
if m.PhotoPath != "" && !fs.IsGenerated(m.PhotoPath) {
|
||||
return txt.FileTitle(m.PhotoPath)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// SubjectNames returns all known subject names.
|
||||
func (m *Photo) SubjectNames() []string {
|
||||
if f, err := m.PrimaryFile(); err == nil {
|
||||
return f.SubjectNames()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
305
internal/entity/photo_title_test.go
Normal file
305
internal/entity/photo_title_test.go
Normal file
|
@ -0,0 +1,305 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPhoto_HasTitle(t *testing.T) {
|
||||
t.Run("false", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo03")
|
||||
assert.False(t, m.HasTitle())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo04")
|
||||
assert.True(t, m.HasTitle())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_NoTitle(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo03")
|
||||
assert.True(t, m.NoTitle())
|
||||
})
|
||||
t.Run("false", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo04")
|
||||
assert.False(t, m.NoTitle())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_SetTitle(t *testing.T) {
|
||||
t.Run("empty title", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
|
||||
m.SetTitle("", SrcManual)
|
||||
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
|
||||
})
|
||||
t.Run("title not from the same source", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
|
||||
m.SetTitle("NewTitleSet", SrcAuto)
|
||||
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
|
||||
})
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
assert.Equal(t, "TitleToBeSet", m.PhotoTitle)
|
||||
m.SetTitle("NewTitleSet", SrcName)
|
||||
assert.Equal(t, "NewTitleSet", m.PhotoTitle)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_UpdateTitle(t *testing.T) {
|
||||
t.Run("wont update title was modified", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo08")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Black beach", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
assert.Equal(t, "Black beach", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location without city and label", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo10")
|
||||
classifyLabels := &classify.Labels{{Name: "tree", Uncertainty: 30, Source: "manual", Priority: 5, Categories: []string{"plant"}}}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// TODO: Unstable
|
||||
if len(m.SubjectNames()) > 0 {
|
||||
assert.Equal(t, "Actor A / Germany / 2016", m.PhotoTitle)
|
||||
} else {
|
||||
assert.Equal(t, "Tree / Germany / 2016", m.PhotoTitle)
|
||||
}
|
||||
})
|
||||
t.Run("photo with location and short city and label", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo09")
|
||||
classifyLabels := &classify.Labels{{Name: "tree", Uncertainty: 30, Source: "manual", Priority: 5, Categories: []string{"plant"}}}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Tree / Teotihuacán / 2016", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location and locname >45", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo13")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "LonglonglonglonglonglonglonglonglonglonglonglonglongName", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location and locname >20", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo14")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "longlonglonglonglonglongName / 2016", m.PhotoTitle)
|
||||
})
|
||||
|
||||
t.Run("photo with location and short city", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo09")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Adosada Platform / Teotihuacán / 2016", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location without city", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo10")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// TODO: Unstable
|
||||
if len(m.SubjectNames()) > 0 {
|
||||
assert.Equal(t, "Actor A / Germany / 2016", m.PhotoTitle)
|
||||
} else {
|
||||
assert.Equal(t, "Holiday Park / Germany / 2016", m.PhotoTitle)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("photo with location without loc name and long city", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo11")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "longlonglonglonglongcity / 2016", m.PhotoTitle)
|
||||
})
|
||||
t.Run("photo with location without loc name and short city", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo12")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "shortcity / Germany / 2016", m.PhotoTitle)
|
||||
})
|
||||
t.Run("no location original name", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("19800101_000002_D640C559")
|
||||
classifyLabels := &classify.Labels{{Name: "classify", Uncertainty: 30, Source: SrcManual, Priority: 5, Categories: []string{"flower", "plant"}}}
|
||||
assert.Equal(t, "Lake / 2790", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Examplefilenameoriginal", m.PhotoTitle)
|
||||
})
|
||||
t.Run("no location", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo01")
|
||||
classifyLabels := &classify.Labels{{Name: "classify", Uncertainty: 30, Source: SrcManual, Priority: 5, Categories: []string{"flower", "plant"}}}
|
||||
assert.Equal(t, "", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Classify / Germany / 2006", m.PhotoTitle)
|
||||
})
|
||||
|
||||
t.Run("no location no labels", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo02")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// TODO: Unstable
|
||||
if len(m.SubjectNames()) > 0 {
|
||||
assert.Equal(t, "Actress A / 1990", m.PhotoTitle)
|
||||
} else {
|
||||
assert.Equal(t, "Bridge / 1990", m.PhotoTitle)
|
||||
}
|
||||
})
|
||||
t.Run("no location no labels no takenAt", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo20")
|
||||
classifyLabels := &classify.Labels{}
|
||||
assert.Equal(t, "", m.PhotoTitle)
|
||||
err := m.UpdateTitle(*classifyLabels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Unknown", m.PhotoTitle)
|
||||
})
|
||||
t.Run("OnePerson", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo10")
|
||||
|
||||
assert.Equal(t, SrcAuto, m.TitleSrc)
|
||||
assert.Equal(t, SrcAuto, m.DescriptionSrc)
|
||||
assert.Equal(t, "Title", m.PhotoTitle)
|
||||
assert.Equal(t, "", m.PhotoDescription)
|
||||
|
||||
err := m.UpdateTitle(classify.Labels{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, SrcAuto, m.TitleSrc)
|
||||
assert.Equal(t, SrcAuto, m.DescriptionSrc)
|
||||
|
||||
// TODO: Unstable
|
||||
if len(m.SubjectNames()) > 0 {
|
||||
assert.Equal(t, "Actor A / Germany / 2016", m.PhotoTitle)
|
||||
} else {
|
||||
assert.Equal(t, "Holiday Park / Germany / 2016", m.PhotoTitle)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", m.PhotoDescription)
|
||||
})
|
||||
t.Run("People", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo04")
|
||||
|
||||
assert.Equal(t, SrcAuto, m.TitleSrc)
|
||||
assert.Equal(t, SrcAuto, m.DescriptionSrc)
|
||||
assert.Equal(t, "Neckarbrücke", m.PhotoTitle)
|
||||
assert.Equal(t, "", m.PhotoDescription)
|
||||
|
||||
err := m.UpdateTitle(classify.Labels{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, SrcAuto, m.TitleSrc)
|
||||
assert.Equal(t, SrcAuto, m.DescriptionSrc)
|
||||
assert.Equal(t, "Corn McCornface & Jens Mander / 2014", m.PhotoTitle)
|
||||
assert.Equal(t, "", m.PhotoDescription)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_FileTitle(t *testing.T) {
|
||||
t.Run("non-latin", func(t *testing.T) {
|
||||
photo := Photo{PhotoName: "桥", PhotoPath: "", OriginalName: ""}
|
||||
result := photo.FileTitle()
|
||||
assert.Equal(t, "桥", result)
|
||||
})
|
||||
t.Run("changing-of-the-guard--buckingham-palace_7925318070_o.jpg", func(t *testing.T) {
|
||||
photo := Photo{PhotoName: "20200102_194030_9EFA9E5E", PhotoPath: "2000/05", OriginalName: "flickr import/changing-of-the-guard--buckingham-palace_7925318070_o.jpg"}
|
||||
result := photo.FileTitle()
|
||||
assert.Equal(t, "Changing of the Guard / Buckingham Palace", result)
|
||||
})
|
||||
t.Run("empty title", func(t *testing.T) {
|
||||
photo := Photo{PhotoName: "", PhotoPath: "", OriginalName: ""}
|
||||
result := photo.FileTitle()
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
t.Run("return title", func(t *testing.T) {
|
||||
photo := Photo{PhotoName: "sun, beach, fun", PhotoPath: "", OriginalName: "", PhotoTitle: ""}
|
||||
result := photo.FileTitle()
|
||||
assert.Equal(t, "Sun, Beach, Fun", result)
|
||||
})
|
||||
t.Run("return title", func(t *testing.T) {
|
||||
photo := Photo{PhotoName: "", PhotoPath: "vacation", OriginalName: "20200102_194030_9EFA9E5E", PhotoTitle: ""}
|
||||
result := photo.FileTitle()
|
||||
assert.Equal(t, "Vacation", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_SubjectNames(t *testing.T) {
|
||||
t.Run("Photo09", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo09")
|
||||
|
||||
if names := m.SubjectNames(); len(names) > 0 {
|
||||
t.Errorf("no name expected: %#v", names)
|
||||
}
|
||||
})
|
||||
t.Run("Photo10", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo10")
|
||||
|
||||
if names := m.SubjectNames(); len(names) == 1 {
|
||||
assert.Equal(t, "Actor A", names[0])
|
||||
} else {
|
||||
t.Logf("unstable subject list: %#v", names)
|
||||
}
|
||||
})
|
||||
t.Run("Photo04", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo04")
|
||||
|
||||
if names := m.SubjectNames(); len(names) != 2 {
|
||||
t.Errorf("two names expected: %#v", names)
|
||||
} else {
|
||||
assert.Equal(t, []string{"Corn McCornface", "Jens Mander"}, names)
|
||||
}
|
||||
})
|
||||
}
|
37
pkg/txt/names.go
Normal file
37
pkg/txt/names.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package txt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UniqueNames removes exact duplicates from a list of strings without changing their order.
|
||||
func UniqueNames(names []string) (result []string) {
|
||||
if len(names) < 1 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
k := make(map[string]bool)
|
||||
|
||||
for _, n := range names {
|
||||
if _, value := k[n]; !value {
|
||||
k[n] = true
|
||||
result = append(result, n)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// JoinNames joins a list of names to be used in titles and descriptions.
|
||||
func JoinNames(names []string) string {
|
||||
if l := len(names); l == 0 {
|
||||
return ""
|
||||
} else if l == 1 {
|
||||
return names[0]
|
||||
} else if l == 2 {
|
||||
return strings.Join(names, " & ")
|
||||
} else {
|
||||
return fmt.Sprintf("%s & %s", strings.Join(names[:l-1], ", "), names[l-1])
|
||||
}
|
||||
}
|
45
pkg/txt/names_test.go
Normal file
45
pkg/txt/names_test.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package txt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUniqueNames(t *testing.T) {
|
||||
t.Run("ManyNames", func(t *testing.T) {
|
||||
result := UniqueNames([]string{"lazy", "jpg", "Brown", "apple", "brown", "new-york", "JPG"})
|
||||
assert.Equal(t, []string{"lazy", "jpg", "Brown", "apple", "brown", "new-york", "JPG"}, result)
|
||||
})
|
||||
t.Run("OneNames", func(t *testing.T) {
|
||||
result := UniqueNames([]string{"foo bar"})
|
||||
assert.Equal(t, []string{"foo bar"}, result)
|
||||
})
|
||||
t.Run("None", func(t *testing.T) {
|
||||
result := UniqueNames(nil)
|
||||
assert.Equal(t, []string{}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestJoinNames(t *testing.T) {
|
||||
t.Run("NoName", func(t *testing.T) {
|
||||
result := JoinNames([]string{})
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
t.Run("OneName", func(t *testing.T) {
|
||||
result := JoinNames([]string{"Jens Mander"})
|
||||
assert.Equal(t, "Jens Mander", result)
|
||||
})
|
||||
t.Run("TwoNames", func(t *testing.T) {
|
||||
result := JoinNames([]string{"Jens Mander", "Name 2"})
|
||||
assert.Equal(t, "Jens Mander & Name 2", result)
|
||||
})
|
||||
t.Run("ThreeNames", func(t *testing.T) {
|
||||
result := JoinNames([]string{"Jens Mander", "Name 2", "Name 3"})
|
||||
assert.Equal(t, "Jens Mander, Name 2 & Name 3", result)
|
||||
})
|
||||
t.Run("ManyNames", func(t *testing.T) {
|
||||
result := JoinNames([]string{"Jens Mander", "Name 2", "Name 3", "Name 4"})
|
||||
assert.Equal(t, "Jens Mander, Name 2, Name 3 & Name 4", result)
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue