diff --git a/internal/form/search_albums.go b/internal/form/search_albums.go index 3411c6ef9..23750c505 100644 --- a/internal/form/search_albums.go +++ b/internal/form/search_albums.go @@ -10,9 +10,9 @@ type SearchAlbums struct { Slug string `form:"slug"` Title string `form:"title"` Country string `json:"country"` - Year string `form:"year" example:"year:1990|2003" notes:"Year Number, OR search with |"` - Month string `form:"month" example:"month:7|10" notes:"Month (1-12), OR search with |"` - Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31), OR search with |"` + Year string `form:"year" example:"year:1990|2003" notes:"Year (separate with |)"` + Month string `form:"month" example:"month:7|10" notes:"Month (1-12, separate with |)"` + Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31, separate with |)"` Favorite bool `form:"favorite"` Public bool `form:"public"` Private bool `form:"private"` diff --git a/internal/form/search_photos.go b/internal/form/search_photos.go index 23775feaa..457f1f5af 100644 --- a/internal/form/search_photos.go +++ b/internal/form/search_photos.go @@ -13,14 +13,14 @@ type SearchPhotos struct { Filter string `form:"filter" serialize:"-" notes:"-"` ID string `form:"id" example:"id:123e4567-e89b-..." notes:"Finds pictures by Exif UID, XMP Document ID or Instance ID"` UID string `form:"uid" example:"uid:pqbcf5j446s0futy" notes:"Limits results to the specified internal unique IDs"` - Type string `form:"type" example:"type:raw" notes:"Media Type (image, video, raw, live, animated); OR search with |"` - Path string `form:"path" example:"path:2020/Holiday" notes:"Path Name, OR search with |, supports * wildcards"` - Folder string `form:"folder" example:"folder:\"*/2020\"" notes:"Path Name, OR search with |, supports * wildcards"` // Alias for Path - Name string `form:"name" example:"name:\"IMG_9831-112*\"" notes:"File Name without path and extension, OR search with |"` - Filename string `form:"filename" example:"filename:\"2021/07/12345.jpg\"" notes:"File Name with path and extension, OR search with |"` - Original string `form:"original" example:"original:\"IMG_9831-112*\"" notes:"Original file name of imported files, OR search with |"` - Title string `form:"title" example:"title:\"Lake*\"" notes:"Title, OR search with |"` - Hash string `form:"hash" example:"hash:2fd4e1c67a2d" notes:"SHA1 File Hash, OR search with |"` + Type string `form:"type" example:"type:raw" notes:"Media Type (image, video, raw, live, animated); separate with |"` + Path string `form:"path" example:"path:2020/Holiday" notes:"Path Name (separate with |), supports * wildcards"` + Folder string `form:"folder" example:"folder:\"*/2020\"" notes:"Path Name (separate with |), supports * wildcards"` // Alias for Path + Name string `form:"name" example:"name:\"IMG_9831-112*\"" notes:"File Name without path and extension (separate with |)"` + Filename string `form:"filename" example:"filename:\"2021/07/12345.jpg\"" notes:"File Name with path and extension (separate with |)"` + Original string `form:"original" example:"original:\"IMG_9831-112*\"" notes:"Original file name of imported files (separate with |)"` + Title string `form:"title" example:"title:\"Lake*\"" notes:"Title (separate with |)"` + Hash string `form:"hash" example:"hash:2fd4e1c67a2d" notes:"SHA1 File Hash (separate with |)"` Primary bool `form:"primary" notes:"Finds primary JPEG files only"` Stack bool `form:"stack" notes:"Finds pictures with more than one media file"` Unstacked bool `form:"unstacked" notes:"Finds pictures with a file that has been removed from a stack"` @@ -56,24 +56,24 @@ type SearchPhotos struct { Diff uint32 `form:"diff" notes:"Differential Perceptual Hash (000000-FFFFFF)"` Mono bool `form:"mono" notes:"Finds pictures with few or no colors"` Geo string `form:"geo" example:"geo:yes" notes:"Finds pictures with or without coordinates"` - Keywords string `form:"keywords" example:"keywords:\"buffalo&water\"" notes:"Keywords, can be combined with & and |"` // Filter by keyword(s) - Label string `form:"label" example:"label:cat|dog" notes:"Label Name, OR search with |"` // Label name - Category string `form:"category" notes:"Location Category Name"` // Moments - Country string `form:"country" example:"country:\"de|us\"" notes:"Country Code, OR search with |"` // Moments - State string `form:"state" example:"state:\"Baden-Württemberg\"" notes:"Name of State (Location), OR search with |"` // Moments - City string `form:"city" example:"city:\"Berlin\"" notes:"Name of City (Location), OR search with |"` // Moments - Year string `form:"year" example:"year:1990|2003" notes:"Year Number, OR search with |"` // Moments - Month string `form:"month" example:"month:7|10" notes:"Month (1-12), OR search with |"` // Moments - Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31), OR search with |"` // Moments + Keywords string `form:"keywords" example:"keywords:\"sand&water\"" notes:"Keywords (combinable with & and |)"` + Label string `form:"label" example:"label:cat|dog" notes:"Label Names (separate with |)"` + Category string `form:"category" example:"category:airport" notes:"Location Category"` + Country string `form:"country" example:"country:\"de|us\"" notes:"Location Country Code (separate with |)"` // Moments + State string `form:"state" example:"state:\"Baden-Württemberg\"" notes:"Location State (separate with |)"` // Moments + City string `form:"city" example:"city:\"Berlin\"" notes:"Location City (separate with |)"` // Moments + Year string `form:"year" example:"year:1990|2003" notes:"Year (separate with |)"` // Moments + Month string `form:"month" example:"month:7|10" notes:"Month (1-12, separate with |)"` // Moments + Day string `form:"day" example:"day:3|13" notes:"Day of Month (1-31, separate with |)"` // Moments Face string `form:"face" example:"face:PN6QO5INYTUSAATOFL43LL2ABAV5ACZG" notes:"Face ID, yes, no, new, or kind"` // UIDs Faces string `form:"faces" example:"faces:yes faces:3" notes:"Minimum number of Faces (yes = 1)"` // Find or exclude faces if detected. Subject string `form:"subject" example:"subject:\"Jane Doe & John Doe\"" notes:"Alias for person"` // UIDs - Person string `form:"person" example:"person:\"Jane Doe & John Doe\"" notes:"Subject Names, exact matches, can be combined with & and |"` // Alias for Subject + Person string `form:"person" example:"person:\"Jane Doe & John Doe\"" notes:"Subject Names, exact matches (combinable with & and |)"` // Alias for Subject Subjects string `form:"subjects" example:"subjects:\"Jane & John\"" notes:"Alias for people"` // People names - People string `form:"people" example:"people:\"Jane & John\"" notes:"Subject Names, can be combined with & and |"` // Alias for Subjects + People string `form:"people" example:"people:\"Jane & John\"" notes:"Subject Names (combinable with & and |)"` // Alias for Subjects Album string `form:"album" example:"album:berlin" notes:"Album UID or Name, supports * wildcards"` // Album UIDs or name - Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names, can be combined with & and |"` // Multi search with and/or - Color string `form:"color" example:"color:\"red|blue\"" notes:"Color Name (purple, magenta, pink, red, orange, gold, yellow, lime, green, teal, cyan, blue, brown, white, grey, black), OR search with |"` // Main color + Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names (combinable with & and |)"` // Multi search with and/or + Color string `form:"color" example:"color:\"red|blue\"" notes:"Color Name (purple, magenta, pink, red, orange, gold, yellow, lime, green, teal, cyan, blue, brown, white, grey, black) (separate with |)"` // Main color Quality int `form:"quality" notes:"Minimum quality score (1-7)"` // Photo quality score Review bool `form:"review" notes:"Finds pictures in review"` // Find photos in review Camera string `form:"camera" example:"camera:canon" notes:"Camera Make/Model Name"` // Camera UID or name diff --git a/internal/form/search_photos_geo.go b/internal/form/search_photos_geo.go index be0777bfa..21808609f 100644 --- a/internal/form/search_photos_geo.go +++ b/internal/form/search_photos_geo.go @@ -53,9 +53,11 @@ type SearchPhotosGeo struct { People string `form:"people"` // Alias for Subjects Chroma int16 `form:"chroma" example:"chroma:70" notes:"Chroma (0-100)"` Mono bool `form:"mono" notes:"Finds pictures with few or no colors"` - Keywords string `form:"keywords"` + Keywords string `form:"keywords" example:"keywords:\"sand&water\"" notes:"Keywords (combinable with & and |)"` + Label string `form:"label" example:"label:cat|dog" notes:"Label Names (separate with |)"` + Category string `form:"category" example:"category:airport" notes:"Location Category"` Album string `form:"album" example:"album:berlin" notes:"Album UID or Name, supports * wildcards"` - Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names, can be combined with & and |"` + Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album Names (combinable with & and |)"` Country string `form:"country"` State string `form:"state"` // Moments City string `form:"city"` diff --git a/internal/form/search_photos_geo_test.go b/internal/form/search_photos_geo_test.go index 3001bd861..dde38b4c6 100644 --- a/internal/form/search_photos_geo_test.go +++ b/internal/form/search_photos_geo_test.go @@ -247,6 +247,16 @@ func TestSearchPhotosGeo_Serialize(t *testing.T) { assert.Equal(t, "q:\"q:fooBar baz\" favorite:true", form.Serialize()) } +func TestSearchPhotosGeo_Unserialize(t *testing.T) { + filter := "public:true label:bay|beach|cape|seashore" + frm := SearchPhotosGeo{} + err := Unserialize(&frm, filter) + assert.Equal(t, true, frm.Public) + assert.Equal(t, "bay|beach|cape|seashore", frm.Label) + assert.NoError(t, err) +} + +// public:true label:bay|beach|cape|seashore func TestSearchPhotosGeo_SerializeAll(t *testing.T) { form := &SearchPhotosGeo{Query: "q:\"fooBar baz\"", Favorite: "true"} diff --git a/internal/search/photos.go b/internal/search/photos.go index 767e473f8..884f9544d 100644 --- a/internal/search/photos.go +++ b/internal/search/photos.go @@ -107,7 +107,8 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string) } else if a.AlbumFilter == "" { s = s.Joins("JOIN photos_albums ON photos_albums.photo_uid = files.photo_uid"). Where("photos_albums.hidden = 0 AND photos_albums.album_uid = ?", a.AlbumUID) - } else if err = form.Unserialize(&f, a.AlbumFilter); err != nil { + } else if formErr := form.Unserialize(&f, a.AlbumFilter); formErr != nil { + log.Debugf("search: %s (%s)", clean.Error(formErr), clean.Log(a.AlbumFilter)) return PhotoResults{}, 0, ErrBadFilter } else { f.Filter = a.AlbumFilter @@ -266,7 +267,7 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string) var labels []entity.Label var labelIds []uint if txt.NotEmpty(f.Label) { - if err := Db().Where(AnySlug("label_slug", f.Label, txt.Or)).Or(AnySlug("custom_slug", f.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || err != nil { + if labelErr := Db().Where(AnySlug("label_slug", f.Label, txt.Or)).Or(AnySlug("custom_slug", f.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || labelErr != nil { log.Debugf("search: label %s not found", txt.LogParamLower(f.Label)) return PhotoResults{}, 0, nil } else { diff --git a/internal/search/photos_geo.go b/internal/search/photos_geo.go index 98beccfad..e381120df 100644 --- a/internal/search/photos_geo.go +++ b/internal/search/photos_geo.go @@ -93,7 +93,8 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes } else if a.AlbumFilter == "" { s = s.Joins("JOIN photos_albums ON photos_albums.photo_uid = files.photo_uid"). Where("photos_albums.hidden = 0 AND photos_albums.album_uid = ?", a.AlbumUID) - } else if err = form.Unserialize(&f, a.AlbumFilter); err != nil { + } else if formErr := form.Unserialize(&f, a.AlbumFilter); formErr != nil { + log.Debugf("search: %s (%s)", clean.Error(formErr), clean.Log(a.AlbumFilter)) return GeoResults{}, ErrBadFilter } else { f.Filter = a.AlbumFilter @@ -199,6 +200,31 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes } } + // Filter by label, label category and keywords. + var categories []entity.Category + var labels []entity.Label + var labelIds []uint + if txt.NotEmpty(f.Label) { + if labelErr := Db().Where(AnySlug("label_slug", f.Label, txt.Or)).Or(AnySlug("custom_slug", f.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || labelErr != nil { + log.Debugf("search: label %s not found", txt.LogParamLower(f.Label)) + return GeoResults{}, nil + } else { + for _, l := range labels { + labelIds = append(labelIds, l.ID) + + Log("find categories", Db().Where("category_id = ?", l.ID).Find(&categories).Error) + log.Debugf("search: label %s includes %d categories", txt.LogParamLower(l.LabelName), len(categories)) + + for _, category := range categories { + labelIds = append(labelIds, category.LabelID) + } + } + + s = s.Joins("JOIN photos_labels ON photos_labels.photo_id = files.photo_id AND photos_labels.uncertainty < 100 AND photos_labels.label_id IN (?)", labelIds). + Group("photos.id, files.id") + } + } + // Set search filters based on search terms. if terms := txt.SearchTerms(f.Query); f.Query != "" && len(terms) == 0 { if f.Title == "" { @@ -444,6 +470,12 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes s = s.Where("places.place_city IN (?)", SplitOr(f.City)) } + // Filter by location category. + if txt.NotEmpty(f.Category) { + s = s.Joins("JOIN cells ON photos.cell_id = cells.id"). + Where("cells.cell_category IN (?)", SplitOr(strings.ToLower(f.Category))) + } + // Filter by media type. if txt.NotEmpty(f.Type) { s = s.Where("photos.photo_type IN (?)", SplitOr(strings.ToLower(f.Type)))