diff --git a/frontend/src/pages/albums.vue b/frontend/src/pages/albums.vue index 32c4c77b6..c6ffd8379 100644 --- a/frontend/src/pages/albums.vue +++ b/frontend/src/pages/albums.vue @@ -61,13 +61,12 @@ - + - + @@ -42,8 +42,12 @@
-

No labels matched your search

-
Try again using a related or otherwise similar term.
+

+ No labels matched your search +

+
+ Try again using a related or otherwise similar term. +
@@ -55,13 +59,12 @@ xs6 sm4 md3 lg2 d-flex > - + - + - - {{ label.LabelName | capitalize }} + + + + {{ label.LabelName | capitalize }} + + + edit + + + star @@ -132,10 +158,15 @@ routeName: routeName, labels: { search: this.$gettext("Search"), + name: this.$gettext("Label Name"), }, + titleRule: v => v.length <= 25 || this.$gettext("Name too long"), }; }, methods: { + onSave(label) { + label.update(); + }, showAll() { this.filter.all = "true"; this.updateQuery(); @@ -148,10 +179,6 @@ this.filter.q = ''; this.updateQuery(); }, - openLabel(index) { - const label = this.results[index]; - this.$router.push({name: "photos", query: {q: "label:" + label.LabelSlug}}); - }, loadMore() { if (this.scrollDisabled) return; diff --git a/internal/api/album.go b/internal/api/album.go index 3203cec58..73129887e 100644 --- a/internal/api/album.go +++ b/internal/api/album.go @@ -144,7 +144,7 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) { conf.Db().Save(&m) event.Publish("config.updated", event.Data(conf.ClientConfig())) - event.Success(fmt.Sprintf("album \"%s\" saved", m.AlbumName)) + event.Success("album saved") PublishAlbumEvent(EntityUpdated, id, c, q) diff --git a/internal/api/errors.go b/internal/api/errors.go index dac41ffa0..624a7473d 100644 --- a/internal/api/errors.go +++ b/internal/api/errors.go @@ -14,5 +14,6 @@ var ( ErrUploadNSFW = gin.H{"code": http.StatusForbidden, "error": txt.UcFirst(config.ErrUploadNSFW.Error())} ErrAlbumNotFound = gin.H{"code": http.StatusNotFound, "error": "Album not found"} 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"} ) diff --git a/internal/api/label.go b/internal/api/label.go index 563ae5b1f..ed9ec4226 100644 --- a/internal/api/label.go +++ b/internal/api/label.go @@ -50,6 +50,42 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) { }) } +// PUT /api/v1/labels/:uuid +func UpdateLabel(router *gin.RouterGroup, conf *config.Config) { + router.PUT("/labels/:uuid", func(c *gin.Context) { + if Unauthorized(c, conf) { + c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + return + } + + var f form.Label + + if err := c.BindJSON(&f); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + return + } + + id := c.Param("uuid") + q := query.New(conf.OriginalsPath(), conf.Db()) + + m, err := q.FindLabelByUUID(id) + + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound) + return + } + + m.Rename(f.LabelName) + conf.Db().Save(&m) + + event.Success("label saved") + + PublishLabelEvent(EntityUpdated, id, c, q) + + c.JSON(http.StatusOK, m) + }) +} + // POST /api/v1/labels/:uuid/like // // Parameters: diff --git a/internal/classify/rules.go b/internal/classify/rules.go index ddc694a66..a8b403719 100644 --- a/internal/classify/rules.go +++ b/internal/classify/rules.go @@ -64,7 +64,7 @@ var rules = LabelRules{ }, "african chameleon": { Label: "chameleon", - Threshold: 0.500000, + Threshold: 0.900000, Priority: 1, Categories: []string{"reptile", "animal", "lizard"}, }, @@ -148,9 +148,9 @@ var rules = LabelRules{ }, "altar": { Label: "", - Threshold: 0.300000, + Threshold: 0.500000, Priority: 0, - Categories: []string{}, + Categories: []string{"church"}, }, "ambulance": { Label: "van", @@ -172,7 +172,7 @@ var rules = LabelRules{ }, "american chameleon": { Label: "chameleon", - Threshold: 0.500000, + Threshold: 0.900000, Priority: 1, Categories: []string{"reptile", "animal", "lizard"}, }, @@ -520,7 +520,7 @@ var rules = LabelRules{ }, "beacon": { Label: "", - Threshold: 0.000000, + Threshold: 0.400000, Priority: 0, Categories: []string{"tower", "architecture"}, }, @@ -664,8 +664,8 @@ var rules = LabelRules{ }, "binoculars": { Label: "", - Threshold: 0.500000, - Priority: 0, + Threshold: 1.000000, + Priority: -2, Categories: []string{}, }, "bird": { @@ -729,10 +729,10 @@ var rules = LabelRules{ Categories: []string{"animal"}, }, "black-footed ferret": { - Label: "", + Label: "animal", Threshold: 0.400000, Priority: 0, - Categories: []string{"animal"}, + Categories: []string{}, }, "blenheim spaniel dog": { Label: "dog", @@ -855,9 +855,9 @@ var rules = LabelRules{ Categories: []string{"animal"}, }, "bow": { - Label: "portrait", - Threshold: 0.200000, - Priority: 0, + Label: "", + Threshold: 1.000000, + Priority: -2, Categories: []string{}, }, "bow tie": { @@ -1240,7 +1240,7 @@ var rules = LabelRules{ }, "chameleon": { Label: "chameleon", - Threshold: 0.500000, + Threshold: 0.900000, Priority: 1, Categories: []string{"reptile", "animal", "lizard"}, }, @@ -1731,10 +1731,10 @@ var rules = LabelRules{ Categories: []string{"reptile", "animal"}, }, "diaper": { - Label: "baby", - Threshold: 0.250000, - Priority: 0, - Categories: []string{"people"}, + Label: "", + Threshold: 1.000000, + Priority: -2, + Categories: []string{}, }, "digital clock": { Label: "display", @@ -3621,10 +3621,10 @@ var rules = LabelRules{ Categories: []string{"vehicle"}, }, "mink": { - Label: "", + Label: "animal", Threshold: 0.400000, Priority: 0, - Categories: []string{"animal"}, + Categories: []string{}, }, "missile": { Label: "", @@ -4293,10 +4293,10 @@ var rules = LabelRules{ Categories: []string{}, }, "polecat": { - Label: "", + Label: "animal", Threshold: 0.400000, Priority: 0, - Categories: []string{"animal"}, + Categories: []string{}, }, "police van": { Label: "van", diff --git a/internal/classify/rules.yml b/internal/classify/rules.yml index 172d2bd34..3a37e8791 100644 --- a/internal/classify/rules.yml +++ b/internal/classify/rules.yml @@ -417,7 +417,7 @@ common iguana: chameleon: label: chameleon - threshold: 0.5 + threshold: 0.9 priority: 1 categories: - reptile @@ -1578,19 +1578,16 @@ weasel: - animal mink: + label: animal threshold: 0.4 - categories: - - animal polecat: + label: animal threshold: 0.4 - categories: - - animal black-footed ferret: + label: animal threshold: 0.4 - categories: - - animal otter: threshold: 0.4 @@ -1873,6 +1870,7 @@ bathtub: see: living beacon: + threshold: 0.4 categories: - tower - architecture @@ -3603,7 +3601,7 @@ bassinet: see: baby diaper: - see: baby + see: ignore patio: label: building @@ -3693,7 +3691,9 @@ sundial: threshold: 0.5 altar: - threshold: 0.3 + threshold: 0.5 + categories: + - church ignore: threshold: 1 @@ -3777,11 +3777,10 @@ barometer: see: display binoculars: - threshold: 0.5 + see: ignore bow: - label: portrait - threshold: 0.2 + see: ignore caldron: label: cooking diff --git a/internal/entity/label.go b/internal/entity/label.go index 9bdce0a75..20fd4a9ea 100644 --- a/internal/entity/label.go +++ b/internal/entity/label.go @@ -8,6 +8,7 @@ import ( "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/pkg/rnd" + "github.com/photoprism/photoprism/pkg/txt" ) // Labels for photo, album and location categorization @@ -68,3 +69,13 @@ func (m *Label) FirstOrCreate(db *gorm.DB) *Label { func (m *Label) AfterCreate(scope *gorm.Scope) error { return scope.SetColumn("New", true) } + +func (m *Label) Rename(name string) { + name = txt.Clip(name, 128) + + if name == "" { + name = txt.SlugToTitle(m.LabelSlug) + } + + m.LabelName = name +} diff --git a/internal/server/routes.go b/internal/server/routes.go index 3d881d996..0f252f6e1 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -39,6 +39,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { api.GetMomentsTime(v1, conf) api.GetLabels(v1, conf) + api.UpdateLabel(v1, conf) api.LikeLabel(v1, conf) api.DislikeLabel(v1, conf) api.LabelThumbnail(v1, conf) diff --git a/pkg/txt/clip.go b/pkg/txt/clip.go new file mode 100644 index 000000000..d269d12dc --- /dev/null +++ b/pkg/txt/clip.go @@ -0,0 +1,18 @@ +package txt + +import "strings" + +func Clip(s string, size int) string { + if s == "" { + return "" + } + + s = strings.TrimSpace(s) + runes := []rune(s) + + if len(runes) > size { + s = string(runes[0:size-1]) + } + + return s +} diff --git a/pkg/txt/clip_test.go b/pkg/txt/clip_test.go new file mode 100644 index 000000000..f3480671f --- /dev/null +++ b/pkg/txt/clip_test.go @@ -0,0 +1,19 @@ +package txt + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClip(t *testing.T) { + t.Run("clip", func(t *testing.T) { + assert.Equal(t, "I'm ä", Clip("I'm ä lazy BRoWN fox!", 6)) + }) + t.Run("ok", func(t *testing.T) { + assert.Equal(t, "I'm ä lazy BRoWN fox!", Clip("I'm ä lazy BRoWN fox!", 128)) + }) + t.Run("empty", func(t *testing.T) { + assert.Equal(t, "", Clip("", -1)) + }) +} diff --git a/pkg/txt/keywords_test.go b/pkg/txt/keywords_test.go deleted file mode 100644 index ac33dcb55..000000000 --- a/pkg/txt/keywords_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package txt - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestKeywords(t *testing.T) { - t.Run("cat", func(t *testing.T) { - keywords := Keywords("cat") - assert.Equal(t, []string{"cat"}, keywords) - }) - t.Run("was", func(t *testing.T) { - keywords := Keywords("was") - assert.Equal(t, []string(nil), keywords) - }) -} diff --git a/pkg/txt/slug.go b/pkg/txt/slug.go new file mode 100644 index 000000000..21f340c66 --- /dev/null +++ b/pkg/txt/slug.go @@ -0,0 +1,13 @@ +package txt + +import "strings" + +// SlugToTitle converts a slug back to a title +func SlugToTitle(s string) string { + if s == "" { + return "" + } + + return Title(strings.Join(Words(s), " ")) +} + diff --git a/pkg/txt/keywords.go b/pkg/txt/words.go similarity index 52% rename from pkg/txt/keywords.go rename to pkg/txt/words.go index ef964ebcd..5c50e690c 100644 --- a/pkg/txt/keywords.go +++ b/pkg/txt/words.go @@ -7,11 +7,14 @@ import ( var KeywordsRegexp = regexp.MustCompile("[\\p{L}]{3,}") -// Keywords extracts keywords for indexing and returns them as string slice. -func Keywords(s string) (results []string) { - all := KeywordsRegexp.FindAllString(s, -1) +// Words returns a slice of words with at least 3 characters from a string. +func Words(s string) (results []string) { + return KeywordsRegexp.FindAllString(s, -1) +} - for _, w := range all { +// Keywords returns a slice of keywords without stopwords. +func Keywords(s string) (results []string) { + for _, w := range Words(s) { w = strings.ToLower(w) if _, ok := Stopwords[w]; ok == false { diff --git a/pkg/txt/words_test.go b/pkg/txt/words_test.go new file mode 100644 index 000000000..041be3023 --- /dev/null +++ b/pkg/txt/words_test.go @@ -0,0 +1,29 @@ +package txt + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWords(t *testing.T) { + t.Run("I'm a lazy brown fox!", func(t *testing.T) { + result := Words("I'm a lazy BRoWN fox!") + assert.Equal(t, []string{"lazy", "BRoWN", "fox"}, result) + }) + t.Run("no result", func(t *testing.T) { + result := Words("x") + assert.Equal(t, []string(nil), result) + }) +} + +func TestKeywords(t *testing.T) { + t.Run("I'm a lazy brown fox!", func(t *testing.T) { + result := Keywords("I'm a lazy BRoWN img!") + assert.Equal(t, []string{"lazy", "brown"}, result) + }) + t.Run("no result", func(t *testing.T) { + result := Keywords("was") + assert.Equal(t, []string(nil), result) + }) +}