From d4b3e456f7c4a1879b2c905a875b95fcb2392d08 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Wed, 11 Dec 2019 07:37:39 +0100 Subject: [PATCH] Backend: Move SQL queries to repo package Signed-off-by: Michael Mayer --- internal/api/albums.go | 43 +- internal/api/download.go | 6 +- internal/api/labels.go | 14 +- internal/api/photos.go | 18 +- internal/api/thumbnails.go | 13 +- internal/api/zip.go | 6 +- internal/photoprism/indexer.go | 4 +- internal/photoprism/search.go | 469 ------------------ internal/photoprism/search_result.go | 136 ----- internal/repo/albums.go | 96 ++++ internal/repo/files.go | 49 ++ internal/repo/labels.go | 125 +++++ internal/repo/photos.go | 324 ++++++++++++ .../search_test.go => repo/photos_test.go} | 4 +- internal/repo/repo.go | 40 ++ 15 files changed, 688 insertions(+), 659 deletions(-) delete mode 100644 internal/photoprism/search.go delete mode 100644 internal/photoprism/search_result.go create mode 100644 internal/repo/albums.go create mode 100644 internal/repo/files.go create mode 100644 internal/repo/labels.go create mode 100644 internal/repo/photos.go rename internal/{photoprism/search_test.go => repo/photos_test.go} (98%) create mode 100644 internal/repo/repo.go diff --git a/internal/api/albums.go b/internal/api/albums.go index 986870efb..660d21c41 100644 --- a/internal/api/albums.go +++ b/internal/api/albums.go @@ -13,11 +13,11 @@ import ( "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/models" + "github.com/photoprism/photoprism/internal/repo" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/util" ) @@ -26,7 +26,7 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) { router.GET("/albums", func(c *gin.Context) { var f form.AlbumSearch - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + r := repo.New(conf.OriginalsPath(), conf.Db()) err := c.MustBindWith(&f, binding.Form) if err != nil { @@ -34,7 +34,7 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) { return } - result, err := search.Albums(f) + result, err := r.Albums(f) if err != nil { c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())}) return @@ -51,8 +51,8 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) { func GetAlbum(router *gin.RouterGroup, conf *config.Config) { router.GET("/albums/:uuid", func(c *gin.Context) { id := c.Param("uuid") - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - m, err := search.FindAlbumByUUID(id) + r := repo.New(conf.OriginalsPath(), conf.Db()) + m, err := r.FindAlbumByUUID(id) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -112,9 +112,9 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) { } id := c.Param("uuid") - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + r := repo.New(conf.OriginalsPath(), conf.Db()) - m, err := search.FindAlbumByUUID(id) + m, err := r.FindAlbumByUUID(id) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -140,9 +140,9 @@ func DeleteAlbum(router *gin.RouterGroup, conf *config.Config) { } id := c.Param("uuid") - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + r := repo.New(conf.OriginalsPath(), conf.Db()) - m, err := search.FindAlbumByUUID(id) + m, err := r.FindAlbumByUUID(id) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -169,9 +169,9 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + r := repo.New(conf.OriginalsPath(), conf.Db()) - album, err := search.FindAlbumByUUID(c.Param("uuid")) + album, err := r.FindAlbumByUUID(c.Param("uuid")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -198,9 +198,8 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - - album, err := search.FindAlbumByUUID(c.Param("uuid")) + r := repo.New(conf.OriginalsPath(), conf.Db()) + album, err := r.FindAlbumByUUID(c.Param("uuid")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -237,8 +236,8 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - a, err := search.FindAlbumByUUID(c.Param("uuid")) + r := repo.New(conf.OriginalsPath(), conf.Db()) + a, err := r.FindAlbumByUUID(c.Param("uuid")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -250,7 +249,7 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) { var failed []string for _, photoUUID := range f.Photos { - if p, err := search.FindPhotoByUUID(photoUUID); err != nil { + if p, err := r.FindPhotoByUUID(photoUUID); err != nil { failed = append(failed, photoUUID) } else { added = append(added, models.NewPhotoAlbum(p.PhotoUUID, a.AlbumUUID).FirstOrCreate(db)) @@ -288,8 +287,8 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - a, err := search.FindAlbumByUUID(c.Param("uuid")) + r := repo.New(conf.OriginalsPath(), conf.Db()) + a, err := r.FindAlbumByUUID(c.Param("uuid")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -312,15 +311,15 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) { start := time.Now() - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - a, err := search.FindAlbumByUUID(c.Param("uuid")) + r := repo.New(conf.OriginalsPath(), conf.Db()) + a, err := r.FindAlbumByUUID(c.Param("uuid")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) return } - p, err := search.Photos(form.PhotoSearch{ + p, err := r.Photos(form.PhotoSearch{ Album: a.AlbumUUID, Count: 10000, Offset: 0, diff --git a/internal/api/download.go b/internal/api/download.go index c95c272f5..349b5f4cd 100644 --- a/internal/api/download.go +++ b/internal/api/download.go @@ -4,10 +4,10 @@ import ( "fmt" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/repo" "github.com/photoprism/photoprism/internal/util" "github.com/gin-gonic/gin" - "github.com/photoprism/photoprism/internal/photoprism" ) // TODO: GET /api/v1/dl/file/:hash @@ -22,8 +22,8 @@ func GetDownload(router *gin.RouterGroup, conf *config.Config) { router.GET("/download/:hash", func(c *gin.Context) { fileHash := c.Param("hash") - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - file, err := search.FindFileByHash(fileHash) + r := repo.New(conf.OriginalsPath(), conf.Db()) + file, err := r.FindFileByHash(fileHash) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) diff --git a/internal/api/labels.go b/internal/api/labels.go index 2d49923a6..2c691ebff 100644 --- a/internal/api/labels.go +++ b/internal/api/labels.go @@ -8,7 +8,7 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/form" - "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/repo" "github.com/photoprism/photoprism/internal/util" ) @@ -17,7 +17,7 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) { router.GET("/labels", func(c *gin.Context) { var f form.LabelSearch - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + r := repo.New(conf.OriginalsPath(), conf.Db()) err := c.MustBindWith(&f, binding.Form) if err != nil { @@ -25,7 +25,7 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) { return } - result, err := search.Labels(f) + result, err := r.Labels(f) if err != nil { c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())}) return @@ -49,9 +49,9 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + r := repo.New(conf.OriginalsPath(), conf.Db()) - label, err := search.FindLabelBySlug(c.Param("slug")) + label, err := r.FindLabelBySlug(c.Param("slug")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -76,9 +76,9 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + r := repo.New(conf.OriginalsPath(), conf.Db()) - label, err := search.FindLabelBySlug(c.Param("slug")) + label, err := r.FindLabelBySlug(c.Param("slug")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) diff --git a/internal/api/photos.go b/internal/api/photos.go index 6348e69c0..00594ff0e 100644 --- a/internal/api/photos.go +++ b/internal/api/photos.go @@ -7,12 +7,12 @@ import ( "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/internal/repo" "github.com/photoprism/photoprism/internal/util" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/photoprism/photoprism/internal/form" - "github.com/photoprism/photoprism/internal/photoprism" ) // GET /api/v1/photos @@ -33,7 +33,7 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) { router.GET("/photos", func(c *gin.Context) { var f form.PhotoSearch - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + r := repo.New(conf.OriginalsPath(), conf.Db()) err := c.MustBindWith(&f, binding.Form) if err != nil { @@ -41,7 +41,7 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) { return } - result, err := search.Photos(f) + result, err := r.Photos(f) if err != nil { c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())}) @@ -61,8 +61,8 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) { // uuid: string PhotoUUID as returned by the API func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) { router.GET("/photos/:uuid/download", func(c *gin.Context) { - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - file, err := search.FindFileByPhotoUUID(c.Param("uuid")) + r := repo.New(conf.OriginalsPath(), conf.Db()) + file, err := r.FindFileByPhotoUUID(c.Param("uuid")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) @@ -100,8 +100,8 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - m, err := search.FindPhotoByUUID(c.Param("uuid")) + r := repo.New(conf.OriginalsPath(), conf.Db()) + m, err := r.FindPhotoByUUID(c.Param("uuid")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -130,8 +130,8 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - m, err := search.FindPhotoByUUID(c.Param("uuid")) + r := repo.New(conf.OriginalsPath(), conf.Db()) + m, err := r.FindPhotoByUUID(c.Param("uuid")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) diff --git a/internal/api/thumbnails.go b/internal/api/thumbnails.go index b2907571a..aa31831d1 100644 --- a/internal/api/thumbnails.go +++ b/internal/api/thumbnails.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/repo" "github.com/photoprism/photoprism/internal/util" "github.com/gin-gonic/gin" @@ -29,8 +30,8 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - file, err := search.FindFileByHash(fileHash) + r := repo.New(conf.OriginalsPath(), conf.Db()) + file, err := r.FindFileByHash(fileHash) if err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) @@ -83,11 +84,11 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + r := repo.New(conf.OriginalsPath(), conf.Db()) // log.Infof("Searching for label slug: %s", c.Param("slug")) - file, err := search.FindLabelThumbBySlug(c.Param("slug")) + file, err := r.FindLabelThumbBySlug(c.Param("slug")) // log.Infof("Label thumb file: %#v", file) @@ -138,9 +139,9 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + r := repo.New(conf.OriginalsPath(), conf.Db()) - file, err := search.FindAlbumThumbByUUID(uuid) + file, err := r.FindAlbumThumbByUUID(uuid) if err != nil { log.Debugf("album has no photos yet, using generic thumb image: %s", uuid) diff --git a/internal/api/zip.go b/internal/api/zip.go index 3f049c9d9..973c0a0fa 100644 --- a/internal/api/zip.go +++ b/internal/api/zip.go @@ -12,10 +12,10 @@ import ( "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/internal/repo" "github.com/photoprism/photoprism/internal/util" "github.com/gin-gonic/gin" - "github.com/photoprism/photoprism/internal/photoprism" ) // POST /api/v1/zip @@ -35,8 +35,8 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) { return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - files, err := search.FindFilesByUUID(f.Photos, 1000, 0) + r := repo.New(conf.OriginalsPath(), conf.Db()) + files, err := r.FindFilesByUUID(f.Photos, 1000, 0) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) diff --git a/internal/photoprism/indexer.go b/internal/photoprism/indexer.go index 412380955..9cb5eed0b 100644 --- a/internal/photoprism/indexer.go +++ b/internal/photoprism/indexer.go @@ -19,13 +19,13 @@ type Indexer struct { // NewIndexer returns a new indexer. // TODO: Is it really necessary to return a pointer? func NewIndexer(conf *config.Config, tensorFlow *TensorFlow) *Indexer { - instance := &Indexer{ + i := &Indexer{ conf: conf, tensorFlow: tensorFlow, db: conf.Db(), } - return instance + return i } func (i *Indexer) originalsPath() string { diff --git a/internal/photoprism/search.go b/internal/photoprism/search.go deleted file mode 100644 index 3721128f9..000000000 --- a/internal/photoprism/search.go +++ /dev/null @@ -1,469 +0,0 @@ -package photoprism - -import ( - "fmt" - "strings" - "time" - - "github.com/gosimple/slug" - "github.com/jinzhu/gorm" - "github.com/photoprism/photoprism/internal/form" - "github.com/photoprism/photoprism/internal/models" - "github.com/photoprism/photoprism/internal/util" -) - -// About 1km ('good enough' for now) -const SearchRadius = 0.009 - -// Search searches given an originals path and a db instance. -type Search struct { - originalsPath string - db *gorm.DB -} - -// SearchCount is the total number of search hits. -type SearchCount struct { - Total int -} - -// NewSearch returns a new Search type with a given path and db instance. -func NewSearch(originalsPath string, db *gorm.DB) *Search { - instance := &Search{ - originalsPath: originalsPath, - db: db, - } - - return instance -} - -// Photos searches for photos based on a Form and returns a PhotoSearchResult slice. -func (s *Search) Photos(f form.PhotoSearch) (results []PhotoSearchResult, err error) { - if err := f.ParseQueryString(); err != nil { - return results, err - } - - defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", f)) - - q := s.db.NewScope(nil).DB() - - // q.LogMode(true) - - q = q.Table("photos"). - Select(`photos.*, - files.id AS file_id, files.file_uuid, files.file_primary, files.file_missing, files.file_name, files.file_hash, - files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio, - files.file_orientation, files.file_main_color, files.file_colors, files.file_luminance, files.file_chroma, - cameras.camera_make, cameras.camera_model, - lenses.lens_make, lenses.lens_model, - countries.country_name, - locations.loc_display_name, locations.loc_name, locations.loc_city, locations.loc_postcode, locations.loc_county, - locations.loc_state, locations.loc_country, locations.loc_country_code, locations.loc_category, locations.loc_type, - GROUP_CONCAT(DISTINCT labels.label_name) AS labels, - GROUP_CONCAT(DISTINCT keywords.keyword) AS keywords`). - Joins("JOIN files ON files.photo_id = photos.id AND files.file_primary AND files.deleted_at IS NULL"). - Joins("JOIN cameras ON cameras.id = photos.camera_id"). - Joins("JOIN lenses ON lenses.id = photos.lens_id"). - Joins("LEFT JOIN countries ON countries.id = photos.country_id"). - Joins("LEFT JOIN locations ON locations.id = photos.location_id"). - Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id"). - Joins("LEFT JOIN labels ON photos_labels.label_id = labels.id"). - Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id"). - Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id"). - Where("photos.deleted_at IS NULL AND files.file_missing = 0"). - Group("photos.id, files.id") - var categories []models.Category - var label models.Label - var labelIds []uint - - if f.Label != "" { - if result := s.db.First(&label, "label_slug = ?", strings.ToLower(f.Label)); result.Error != nil { - log.Errorf("search: label \"%s\" not found", f.Label) - return results, fmt.Errorf("label \"%s\" not found", f.Label) - } else { - labelIds = append(labelIds, label.ID) - - s.db.Where("category_id = ?", label.ID).Find(&categories) - - for _, category := range categories { - labelIds = append(labelIds, category.LabelID) - } - - q = q.Where("labels.id IN (?)", labelIds) - } - } - - if f.Location == true { - q = q.Where("location_id > 0") - - if f.Query != "" { - likeString := "%" + strings.ToLower(f.Query) + "%" - q = q.Where("LOWER(locations.loc_display_name) LIKE ?", likeString) - } - } else if f.Query != "" { - slugString := slug.Make(f.Query) - lowerString := strings.ToLower(f.Query) - likeString := lowerString + "%" - - if result := s.db.First(&label, "label_slug = ?", slugString); result.Error != nil { - log.Infof("search: label \"%s\" not found", f.Query) - - q = q.Where("labels.label_slug = ? OR keywords.keyword LIKE ? OR files.file_main_color = ?", slugString, likeString, lowerString) - } else { - labelIds = append(labelIds, label.ID) - - s.db.Where("category_id = ?", label.ID).Find(&categories) - - for _, category := range categories { - labelIds = append(labelIds, category.LabelID) - } - - log.Infof("search: label \"%s\" includes %d categories", label.LabelName, len(labelIds)) - - q = q.Where("labels.id IN (?) OR keywords.keyword LIKE ? OR files.file_main_color = ?", labelIds, likeString, lowerString) - } - } - - if f.Album != "" { - q = q.Joins("JOIN photos_albums ON photos_albums.photo_uuid = photos.photo_uuid").Where("photos_albums.album_uuid = ?", f.Album) - } - - if f.Camera > 0 { - q = q.Where("photos.camera_id = ?", f.Camera) - } - - if f.Color != "" { - q = q.Where("files.file_main_color = ?", strings.ToLower(f.Color)) - } - - if f.Favorites { - q = q.Where("photos.photo_favorite = 1") - } - - if f.Country != "" { - q = q.Where("locations.loc_country_code = ?", f.Country) - } - - if f.Title != "" { - q = q.Where("LOWER(photos.photo_title) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Title))) - } - - if f.Description != "" { - q = q.Where("LOWER(photos.photo_description) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Description))) - } - - if f.Notes != "" { - q = q.Where("LOWER(photos.photo_notes) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Notes))) - } - - if f.Hash != "" { - q = q.Where("files.file_hash = ?", f.Hash) - } - - if f.Duplicate { - q = q.Where("files.file_duplicate = 1") - } - - if f.Portrait { - q = q.Where("files.file_portrait = 1") - } - - if f.Mono { - q = q.Where("files.file_chroma = 0") - } else if f.Chroma > 9 { - q = q.Where("files.file_chroma > ?", f.Chroma) - } else if f.Chroma > 0 { - q = q.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma) - } - - if f.Fmin > 0 { - q = q.Where("photos.photo_f_number >= ?", f.Fmin) - } - - if f.Fmax > 0 { - q = q.Where("photos.photo_f_number <= ?", f.Fmax) - } - - if f.Dist == 0 { - f.Dist = 20 - } else if f.Dist > 1000 { - f.Dist = 1000 - } - - // Inaccurate distance search, but probably 'good enough' for now - if f.Lat > 0 { - latMin := f.Lat - SearchRadius*float64(f.Dist) - latMax := f.Lat + SearchRadius*float64(f.Dist) - q = q.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax) - } - - if f.Long > 0 { - longMin := f.Long - SearchRadius*float64(f.Dist) - longMax := f.Long + SearchRadius*float64(f.Dist) - q = q.Where("photos.photo_long BETWEEN ? AND ?", longMin, longMax) - } - - if !f.Before.IsZero() { - q = q.Where("photos.taken_at <= ?", f.Before.Format("2006-01-02")) - } - - if !f.After.IsZero() { - q = q.Where("photos.taken_at >= ?", f.After.Format("2006-01-02")) - } - - switch f.Order { - case "newest": - q = q.Order("taken_at DESC") - case "oldest": - q = q.Order("taken_at") - case "imported": - q = q.Order("created_at DESC") - default: - q = q.Order("taken_at DESC") - } - - if f.Count > 0 && f.Count <= 1000 { - q = q.Limit(f.Count).Offset(f.Offset) - } else { - q = q.Limit(100).Offset(0) - } - - if result := q.Scan(&results); result.Error != nil { - return results, result.Error - } - - return results, nil -} - -// FindFiles finds files returning maximum results defined by limit -// and finding them from an offest defined by offset. -func (s *Search) FindFiles(limit int, offset int) (files []models.File, err error) { - if err := s.db.Where(&models.File{}).Limit(limit).Offset(offset).Find(&files).Error; err != nil { - return files, err - } - - return files, nil -} - -// FindFilesByUUID -func (s *Search) FindFilesByUUID(u []string, limit int, offset int) (files []models.File, err error) { - if err := s.db.Where("(photo_uuid IN (?) AND file_primary = 1) OR file_uuid IN (?)", u, u).Preload("Photo").Limit(limit).Offset(offset).Find(&files).Error; err != nil { - return files, err - } - - return files, nil -} - -// FindFileByPhotoUUID -func (s *Search) FindFileByPhotoUUID(u string) (file models.File, err error) { - if err := s.db.Where("photo_uuid = ? AND file_primary = 1", u).Preload("Photo").First(&file).Error; err != nil { - return file, err - } - - return file, nil -} - -// FindFileByID returns a mediafile given a certain ID. -func (s *Search) FindFileByID(id string) (file models.File, err error) { - if err := s.db.Where("id = ?", id).Preload("Photo").First(&file).Error; err != nil { - return file, err - } - - return file, nil -} - -// FindFileByHash finds a file with a given hash string. -func (s *Search) FindFileByHash(fileHash string) (file models.File, err error) { - if err := s.db.Where("file_hash = ?", fileHash).Preload("Photo").First(&file).Error; err != nil { - return file, err - } - - return file, nil -} - -// FindPhotoByID returns a Photo based on the ID. -func (s *Search) FindPhotoByID(photoID uint64) (photo models.Photo, err error) { - if err := s.db.Where("id = ?", photoID).First(&photo).Error; err != nil { - return photo, err - } - - return photo, nil -} - -// FindPhotoByUUID returns a Photo based on the UUID. -func (s *Search) FindPhotoByUUID(photoUUID string) (photo models.Photo, err error) { - if err := s.db.Where("photo_uuid = ?", photoUUID).First(&photo).Error; err != nil { - return photo, err - } - - return photo, nil -} - -// FindLabelBySlug returns a Label based on the slug name. -func (s *Search) FindLabelBySlug(labelSlug string) (label models.Label, err error) { - if err := s.db.Where("label_slug = ?", labelSlug).First(&label).Error; err != nil { - return label, err - } - - return label, nil -} - -// FindLabelThumbBySlug returns a label preview file based on the slug name. -func (s *Search) FindLabelThumbBySlug(labelSlug string) (file models.File, err error) { - // s.db.LogMode(true) - - if err := s.db.Where("files.file_primary AND files.deleted_at IS NULL"). - Joins("JOIN labels ON labels.label_slug = ?", labelSlug). - Joins("JOIN photos_labels ON photos_labels.label_id = labels.id AND photos_labels.photo_id = files.photo_id"). - Order("photos_labels.label_uncertainty ASC"). - First(&file).Error; err != nil { - return file, err - } - - return file, nil -} - -// Labels searches labels based on their name. -func (s *Search) Labels(f form.LabelSearch) (results []LabelSearchResult, err error) { - if err := f.ParseQueryString(); err != nil { - return results, err - } - - defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", f)) - - q := s.db.NewScope(nil).DB() - - // q.LogMode(true) - - q = q.Table("labels"). - Select(`labels.*, COUNT(photos_labels.label_id) AS label_count`). - Joins("JOIN photos_labels ON photos_labels.label_id = labels.id"). - Where("labels.deleted_at IS NULL"). - Group("labels.id") - - if f.Query != "" { - var labelIds []uint - var categories []models.Category - var label models.Label - - likeString := "%" + strings.ToLower(f.Query) + "%" - - if result := s.db.First(&label, "LOWER(label_name) LIKE LOWER(?)", f.Query); result.Error != nil { - log.Infof("search: label \"%s\" not found", f.Query) - - q = q.Where("LOWER(labels.label_name) LIKE ?", likeString) - } else { - labelIds = append(labelIds, label.ID) - - s.db.Where("category_id = ?", label.ID).Find(&categories) - - for _, category := range categories { - labelIds = append(labelIds, category.LabelID) - } - - log.Infof("search: label \"%s\" includes %d categories", label.LabelName, len(labelIds)) - - q = q.Where("labels.id IN (?) OR LOWER(labels.label_name) LIKE ?", labelIds, likeString) - } - } - - if f.Favorites { - q = q.Where("labels.label_favorite = 1") - } - - if f.Priority != 0 { - q = q.Where("labels.label_priority > ?", f.Priority) - } else { - q = q.Where("labels.label_priority >= -2") - } - - switch f.Order { - case "slug": - q = q.Order("labels.label_favorite DESC, label_slug ASC") - default: - q = q.Order("labels.label_favorite DESC, labels.label_priority DESC, label_count DESC, labels.created_at DESC") - } - - if f.Count > 0 && f.Count <= 1000 { - q = q.Limit(f.Count).Offset(f.Offset) - } else { - q = q.Limit(100).Offset(0) - } - - if result := q.Scan(&results); result.Error != nil { - return results, result.Error - } - - return results, nil -} - -/***************** Albums *****************/ - -// FindAlbumByUUID returns a Album based on the UUID. -func (s *Search) FindAlbumByUUID(albumUUID string) (album models.Album, err error) { - if err := s.db.Where("album_uuid = ?", albumUUID).First(&album).Error; err != nil { - return album, err - } - - return album, nil -} - -// FindAlbumThumbByUUID returns a album preview file based on the uuid. -func (s *Search) FindAlbumThumbByUUID(albumUUID string) (file models.File, err error) { - // s.db.LogMode(true) - - if err := s.db.Where("files.file_primary AND files.deleted_at IS NULL"). - Joins("JOIN albums ON albums.album_uuid = ?", albumUUID). - Joins("JOIN photos_albums pa ON pa.album_uuid = albums.album_uuid AND pa.photo_uuid = files.photo_uuid"). - First(&file).Error; err != nil { - return file, err - } - - return file, nil -} - -// Albums searches albums based on their name. -func (s *Search) Albums(f form.AlbumSearch) (results []AlbumSearchResult, err error) { - if err := f.ParseQueryString(); err != nil { - return results, err - } - - defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", f)) - - q := s.db.NewScope(nil).DB() - - // q.LogMode(true) - - q = q.Table("albums"). - Select(`albums.*, COUNT(photos_albums.album_uuid) AS album_count`). - Joins("LEFT JOIN photos_albums ON photos_albums.album_uuid = albums.album_uuid"). - Where("albums.deleted_at IS NULL"). - Group("albums.id") - - if f.Query != "" { - likeString := "%" + strings.ToLower(f.Query) + "%" - q = q.Where("LOWER(albums.album_name) LIKE ?", likeString) - } - - if f.Favorites { - q = q.Where("albums.album_favorite = 1") - } - - switch f.Order { - case "slug": - q = q.Order("albums.album_favorite DESC, album_slug ASC") - default: - q = q.Order("albums.album_favorite DESC, album_count DESC, albums.created_at DESC") - } - - if f.Count > 0 && f.Count <= 1000 { - q = q.Limit(f.Count).Offset(f.Offset) - } else { - q = q.Limit(100).Offset(0) - } - - if result := q.Scan(&results); result.Error != nil { - return results, result.Error - } - - return results, nil -} diff --git a/internal/photoprism/search_result.go b/internal/photoprism/search_result.go deleted file mode 100644 index fb64d5774..000000000 --- a/internal/photoprism/search_result.go +++ /dev/null @@ -1,136 +0,0 @@ -package photoprism - -import ( - "fmt" - "strings" - "time" - - "github.com/gosimple/slug" -) - -// PhotoSearchResult contains found photos and their main file plus other meta data. -type PhotoSearchResult struct { - // Photo - ID uint - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt time.Time - TakenAt time.Time - TakenAtLocal time.Time - TimeZone string - PhotoUUID string - PhotoPath string - PhotoName string - PhotoTitle string - PhotoDescription string - PhotoArtist string - PhotoKeywords string - PhotoColors string - PhotoColor string - PhotoFavorite bool - PhotoPrivate bool - PhotoSensitive bool - PhotoStory bool - PhotoLat float64 - PhotoLong float64 - PhotoAltitude int - PhotoFocalLength int - PhotoIso int - PhotoFNumber float64 - PhotoExposure string - - // Camera - CameraID uint - CameraModel string - CameraMake string - - // Lens - LensID uint - LensModel string - LensMake string - - // Country - CountryID string - CountryName string - - // Location - LocationID uint - LocDisplayName string - LocName string - LocCity string - LocPostcode string - LocCounty string - LocState string - LocCountry string - LocCountryCode string - LocCategory string - LocType string - LocationChanged bool - LocationEstimated bool - - // File - FileID uint - FileUUID string - FilePrimary bool - FileMissing bool - FileName string - FileHash string - FilePerceptualHash string - FileType string - FileMime string - FileWidth int - FileHeight int - FileOrientation int - FileAspectRatio float64 - - // List of matching labels and keywords - Labels string - Keywords string -} - -func (m *PhotoSearchResult) DownloadFileName() string { - var name string - - if m.PhotoTitle != "" { - name = strings.Title(slug.MakeLang(m.PhotoTitle, "en")) - } else { - name = m.PhotoUUID - } - - taken := m.TakenAt.Format("20060102-150405") - - result := fmt.Sprintf("%s-%s.%s", taken, name, m.FileType) - - return result -} - -// LabelSearchResult contains found labels -type LabelSearchResult struct { - // Label - ID uint - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt time.Time - LabelSlug string - LabelName string - LabelPriority int - LabelCount int - LabelFavorite bool - LabelDescription string - LabelNotes string -} - -// AlbumSearchResult contains found albums -type AlbumSearchResult struct { - ID uint - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt time.Time - AlbumUUID string - AlbumSlug string - AlbumName string - AlbumCount int - AlbumFavorite bool - AlbumDescription string - AlbumNotes string -} diff --git a/internal/repo/albums.go b/internal/repo/albums.go new file mode 100644 index 000000000..4266974a2 --- /dev/null +++ b/internal/repo/albums.go @@ -0,0 +1,96 @@ +package repo + +import ( + "fmt" + "strings" + "time" + + "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/internal/models" + "github.com/photoprism/photoprism/internal/util" +) + +// AlbumResult contains found albums +type AlbumResult struct { + ID uint + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time + AlbumUUID string + AlbumSlug string + AlbumName string + AlbumCount int + AlbumFavorite bool + AlbumDescription string + AlbumNotes string +} + +// FindAlbumByUUID returns a Album based on the UUID. +func (s *Repo) FindAlbumByUUID(albumUUID string) (album models.Album, err error) { + if err := s.db.Where("album_uuid = ?", albumUUID).First(&album).Error; err != nil { + return album, err + } + + return album, nil +} + +// FindAlbumThumbByUUID returns a album preview file based on the uuid. +func (s *Repo) FindAlbumThumbByUUID(albumUUID string) (file models.File, err error) { + // s.db.LogMode(true) + + if err := s.db.Where("files.file_primary AND files.deleted_at IS NULL"). + Joins("JOIN albums ON albums.album_uuid = ?", albumUUID). + Joins("JOIN photos_albums pa ON pa.album_uuid = albums.album_uuid AND pa.photo_uuid = files.photo_uuid"). + First(&file).Error; err != nil { + return file, err + } + + return file, nil +} + +// Albums searches albums based on their name. +func (s *Repo) Albums(f form.AlbumSearch) (results []AlbumResult, err error) { + if err := f.ParseQueryString(); err != nil { + return results, err + } + + defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", f)) + + q := s.db.NewScope(nil).DB() + + // q.LogMode(true) + + q = q.Table("albums"). + Select(`albums.*, COUNT(photos_albums.album_uuid) AS album_count`). + Joins("LEFT JOIN photos_albums ON photos_albums.album_uuid = albums.album_uuid"). + Where("albums.deleted_at IS NULL"). + Group("albums.id") + + if f.Query != "" { + likeString := "%" + strings.ToLower(f.Query) + "%" + q = q.Where("LOWER(albums.album_name) LIKE ?", likeString) + } + + if f.Favorites { + q = q.Where("albums.album_favorite = 1") + } + + switch f.Order { + case "slug": + q = q.Order("albums.album_favorite DESC, album_slug ASC") + default: + q = q.Order("albums.album_favorite DESC, album_count DESC, albums.created_at DESC") + } + + if f.Count > 0 && f.Count <= 1000 { + q = q.Limit(f.Count).Offset(f.Offset) + } else { + q = q.Limit(100).Offset(0) + } + + if result := q.Scan(&results); result.Error != nil { + return results, result.Error + } + + return results, nil +} diff --git a/internal/repo/files.go b/internal/repo/files.go new file mode 100644 index 000000000..31f38d551 --- /dev/null +++ b/internal/repo/files.go @@ -0,0 +1,49 @@ +package repo + +import "github.com/photoprism/photoprism/internal/models" + +// FindFiles finds files returning maximum results defined by limit +// and finding them from an offest defined by offset. +func (s *Repo) FindFiles(limit int, offset int) (files []models.File, err error) { + if err := s.db.Where(&models.File{}).Limit(limit).Offset(offset).Find(&files).Error; err != nil { + return files, err + } + + return files, nil +} + +// FindFilesByUUID +func (s *Repo) FindFilesByUUID(u []string, limit int, offset int) (files []models.File, err error) { + if err := s.db.Where("(photo_uuid IN (?) AND file_primary = 1) OR file_uuid IN (?)", u, u).Preload("Photo").Limit(limit).Offset(offset).Find(&files).Error; err != nil { + return files, err + } + + return files, nil +} + +// FindFileByPhotoUUID +func (s *Repo) FindFileByPhotoUUID(u string) (file models.File, err error) { + if err := s.db.Where("photo_uuid = ? AND file_primary = 1", u).Preload("Photo").First(&file).Error; err != nil { + return file, err + } + + return file, nil +} + +// FindFileByID returns a mediafile given a certain ID. +func (s *Repo) FindFileByID(id string) (file models.File, err error) { + if err := s.db.Where("id = ?", id).Preload("Photo").First(&file).Error; err != nil { + return file, err + } + + return file, nil +} + +// FindFileByHash finds a file with a given hash string. +func (s *Repo) FindFileByHash(fileHash string) (file models.File, err error) { + if err := s.db.Where("file_hash = ?", fileHash).Preload("Photo").First(&file).Error; err != nil { + return file, err + } + + return file, nil +} diff --git a/internal/repo/labels.go b/internal/repo/labels.go new file mode 100644 index 000000000..a1c378695 --- /dev/null +++ b/internal/repo/labels.go @@ -0,0 +1,125 @@ +package repo + +import ( + "fmt" + "strings" + "time" + + "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/internal/models" + "github.com/photoprism/photoprism/internal/util" +) + +// LabelResult contains found labels +type LabelResult struct { + // Label + ID uint + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time + LabelSlug string + LabelName string + LabelPriority int + LabelCount int + LabelFavorite bool + LabelDescription string + LabelNotes string +} + +// FindLabelBySlug returns a Label based on the slug name. +func (s *Repo) FindLabelBySlug(labelSlug string) (label models.Label, err error) { + if err := s.db.Where("label_slug = ?", labelSlug).First(&label).Error; err != nil { + return label, err + } + + return label, nil +} + +// FindLabelThumbBySlug returns a label preview file based on the slug name. +func (s *Repo) FindLabelThumbBySlug(labelSlug string) (file models.File, err error) { + // s.db.LogMode(true) + + if err := s.db.Where("files.file_primary AND files.deleted_at IS NULL"). + Joins("JOIN labels ON labels.label_slug = ?", labelSlug). + Joins("JOIN photos_labels ON photos_labels.label_id = labels.id AND photos_labels.photo_id = files.photo_id"). + Order("photos_labels.label_uncertainty ASC"). + First(&file).Error; err != nil { + return file, err + } + + return file, nil +} + +// Labels searches labels based on their name. +func (s *Repo) Labels(f form.LabelSearch) (results []LabelResult, err error) { + if err := f.ParseQueryString(); err != nil { + return results, err + } + + defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", f)) + + q := s.db.NewScope(nil).DB() + + // q.LogMode(true) + + q = q.Table("labels"). + Select(`labels.*, COUNT(photos_labels.label_id) AS label_count`). + Joins("JOIN photos_labels ON photos_labels.label_id = labels.id"). + Where("labels.deleted_at IS NULL"). + Group("labels.id") + + if f.Query != "" { + var labelIds []uint + var categories []models.Category + var label models.Label + + likeString := "%" + strings.ToLower(f.Query) + "%" + + if result := s.db.First(&label, "LOWER(label_name) LIKE LOWER(?)", f.Query); result.Error != nil { + log.Infof("search: label \"%s\" not found", f.Query) + + q = q.Where("LOWER(labels.label_name) LIKE ?", likeString) + } else { + labelIds = append(labelIds, label.ID) + + s.db.Where("category_id = ?", label.ID).Find(&categories) + + for _, category := range categories { + labelIds = append(labelIds, category.LabelID) + } + + log.Infof("search: label \"%s\" includes %d categories", label.LabelName, len(labelIds)) + + q = q.Where("labels.id IN (?) OR LOWER(labels.label_name) LIKE ?", labelIds, likeString) + } + } + + if f.Favorites { + q = q.Where("labels.label_favorite = 1") + } + + if f.Priority != 0 { + q = q.Where("labels.label_priority > ?", f.Priority) + } else { + q = q.Where("labels.label_priority >= -2") + } + + switch f.Order { + case "slug": + q = q.Order("labels.label_favorite DESC, label_slug ASC") + default: + q = q.Order("labels.label_favorite DESC, labels.label_priority DESC, label_count DESC, labels.created_at DESC") + } + + if f.Count > 0 && f.Count <= 1000 { + q = q.Limit(f.Count).Offset(f.Offset) + } else { + q = q.Limit(100).Offset(0) + } + + if result := q.Scan(&results); result.Error != nil { + return results, result.Error + } + + return results, nil +} diff --git a/internal/repo/photos.go b/internal/repo/photos.go new file mode 100644 index 000000000..dd7da0bc4 --- /dev/null +++ b/internal/repo/photos.go @@ -0,0 +1,324 @@ +package repo + +import ( + "fmt" + "strings" + "time" + + "github.com/gosimple/slug" + "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/internal/models" + "github.com/photoprism/photoprism/internal/util" +) + +// PhotoResult contains found photos and their main file plus other meta data. +type PhotoResult struct { + // Photo + ID uint + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time + TakenAt time.Time + TakenAtLocal time.Time + TimeZone string + PhotoUUID string + PhotoPath string + PhotoName string + PhotoTitle string + PhotoDescription string + PhotoArtist string + PhotoKeywords string + PhotoColors string + PhotoColor string + PhotoFavorite bool + PhotoPrivate bool + PhotoSensitive bool + PhotoStory bool + PhotoLat float64 + PhotoLong float64 + PhotoAltitude int + PhotoFocalLength int + PhotoIso int + PhotoFNumber float64 + PhotoExposure string + + // Camera + CameraID uint + CameraModel string + CameraMake string + + // Lens + LensID uint + LensModel string + LensMake string + + // Country + CountryID string + CountryName string + + // Location + LocationID uint + LocDisplayName string + LocName string + LocCity string + LocPostcode string + LocCounty string + LocState string + LocCountry string + LocCountryCode string + LocCategory string + LocType string + LocationChanged bool + LocationEstimated bool + + // File + FileID uint + FileUUID string + FilePrimary bool + FileMissing bool + FileName string + FileHash string + FilePerceptualHash string + FileType string + FileMime string + FileWidth int + FileHeight int + FileOrientation int + FileAspectRatio float64 + + // List of matching labels and keywords + Labels string + Keywords string +} + +func (m *PhotoResult) DownloadFileName() string { + var name string + + if m.PhotoTitle != "" { + name = strings.Title(slug.MakeLang(m.PhotoTitle, "en")) + } else { + name = m.PhotoUUID + } + + taken := m.TakenAt.Format("20060102-150405") + + result := fmt.Sprintf("%s-%s.%s", taken, name, m.FileType) + + return result +} + +// Photos searches for photos based on a Form and returns a PhotoResult slice. +func (s *Repo) Photos(f form.PhotoSearch) (results []PhotoResult, err error) { + if err := f.ParseQueryString(); err != nil { + return results, err + } + + defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", f)) + + q := s.db.NewScope(nil).DB() + + // q.LogMode(true) + + q = q.Table("photos"). + Select(`photos.*, + files.id AS file_id, files.file_uuid, files.file_primary, files.file_missing, files.file_name, files.file_hash, + files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio, + files.file_orientation, files.file_main_color, files.file_colors, files.file_luminance, files.file_chroma, + cameras.camera_make, cameras.camera_model, + lenses.lens_make, lenses.lens_model, + countries.country_name, + locations.loc_display_name, locations.loc_name, locations.loc_city, locations.loc_postcode, locations.loc_county, + locations.loc_state, locations.loc_country, locations.loc_country_code, locations.loc_category, locations.loc_type, + GROUP_CONCAT(DISTINCT labels.label_name) AS labels, + GROUP_CONCAT(DISTINCT keywords.keyword) AS keywords`). + Joins("JOIN files ON files.photo_id = photos.id AND files.file_primary AND files.deleted_at IS NULL"). + Joins("JOIN cameras ON cameras.id = photos.camera_id"). + Joins("JOIN lenses ON lenses.id = photos.lens_id"). + Joins("LEFT JOIN countries ON countries.id = photos.country_id"). + Joins("LEFT JOIN locations ON locations.id = photos.location_id"). + Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id"). + Joins("LEFT JOIN labels ON photos_labels.label_id = labels.id"). + Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id"). + Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id"). + Where("photos.deleted_at IS NULL AND files.file_missing = 0"). + Group("photos.id, files.id") + var categories []models.Category + var label models.Label + var labelIds []uint + + if f.Label != "" { + if result := s.db.First(&label, "label_slug = ?", strings.ToLower(f.Label)); result.Error != nil { + log.Errorf("search: label \"%s\" not found", f.Label) + return results, fmt.Errorf("label \"%s\" not found", f.Label) + } else { + labelIds = append(labelIds, label.ID) + + s.db.Where("category_id = ?", label.ID).Find(&categories) + + for _, category := range categories { + labelIds = append(labelIds, category.LabelID) + } + + q = q.Where("labels.id IN (?)", labelIds) + } + } + + if f.Location == true { + q = q.Where("location_id > 0") + + if f.Query != "" { + likeString := "%" + strings.ToLower(f.Query) + "%" + q = q.Where("LOWER(locations.loc_display_name) LIKE ?", likeString) + } + } else if f.Query != "" { + slugString := slug.Make(f.Query) + lowerString := strings.ToLower(f.Query) + likeString := lowerString + "%" + + if result := s.db.First(&label, "label_slug = ?", slugString); result.Error != nil { + log.Infof("search: label \"%s\" not found", f.Query) + + q = q.Where("labels.label_slug = ? OR keywords.keyword LIKE ? OR files.file_main_color = ?", slugString, likeString, lowerString) + } else { + labelIds = append(labelIds, label.ID) + + s.db.Where("category_id = ?", label.ID).Find(&categories) + + for _, category := range categories { + labelIds = append(labelIds, category.LabelID) + } + + log.Infof("search: label \"%s\" includes %d categories", label.LabelName, len(labelIds)) + + q = q.Where("labels.id IN (?) OR keywords.keyword LIKE ? OR files.file_main_color = ?", labelIds, likeString, lowerString) + } + } + + if f.Album != "" { + q = q.Joins("JOIN photos_albums ON photos_albums.photo_uuid = photos.photo_uuid").Where("photos_albums.album_uuid = ?", f.Album) + } + + if f.Camera > 0 { + q = q.Where("photos.camera_id = ?", f.Camera) + } + + if f.Color != "" { + q = q.Where("files.file_main_color = ?", strings.ToLower(f.Color)) + } + + if f.Favorites { + q = q.Where("photos.photo_favorite = 1") + } + + if f.Country != "" { + q = q.Where("locations.loc_country_code = ?", f.Country) + } + + if f.Title != "" { + q = q.Where("LOWER(photos.photo_title) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Title))) + } + + if f.Description != "" { + q = q.Where("LOWER(photos.photo_description) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Description))) + } + + if f.Notes != "" { + q = q.Where("LOWER(photos.photo_notes) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Notes))) + } + + if f.Hash != "" { + q = q.Where("files.file_hash = ?", f.Hash) + } + + if f.Duplicate { + q = q.Where("files.file_duplicate = 1") + } + + if f.Portrait { + q = q.Where("files.file_portrait = 1") + } + + if f.Mono { + q = q.Where("files.file_chroma = 0") + } else if f.Chroma > 9 { + q = q.Where("files.file_chroma > ?", f.Chroma) + } else if f.Chroma > 0 { + q = q.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma) + } + + if f.Fmin > 0 { + q = q.Where("photos.photo_f_number >= ?", f.Fmin) + } + + if f.Fmax > 0 { + q = q.Where("photos.photo_f_number <= ?", f.Fmax) + } + + if f.Dist == 0 { + f.Dist = 20 + } else if f.Dist > 1000 { + f.Dist = 1000 + } + + // Inaccurate distance search, but probably 'good enough' for now + if f.Lat > 0 { + latMin := f.Lat - SearchRadius*float64(f.Dist) + latMax := f.Lat + SearchRadius*float64(f.Dist) + q = q.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax) + } + + if f.Long > 0 { + longMin := f.Long - SearchRadius*float64(f.Dist) + longMax := f.Long + SearchRadius*float64(f.Dist) + q = q.Where("photos.photo_long BETWEEN ? AND ?", longMin, longMax) + } + + if !f.Before.IsZero() { + q = q.Where("photos.taken_at <= ?", f.Before.Format("2006-01-02")) + } + + if !f.After.IsZero() { + q = q.Where("photos.taken_at >= ?", f.After.Format("2006-01-02")) + } + + switch f.Order { + case "newest": + q = q.Order("taken_at DESC") + case "oldest": + q = q.Order("taken_at") + case "imported": + q = q.Order("created_at DESC") + default: + q = q.Order("taken_at DESC") + } + + if f.Count > 0 && f.Count <= 1000 { + q = q.Limit(f.Count).Offset(f.Offset) + } else { + q = q.Limit(100).Offset(0) + } + + if result := q.Scan(&results); result.Error != nil { + return results, result.Error + } + + return results, nil +} + +// FindPhotoByID returns a Photo based on the ID. +func (s *Repo) FindPhotoByID(photoID uint64) (photo models.Photo, err error) { + if err := s.db.Where("id = ?", photoID).First(&photo).Error; err != nil { + return photo, err + } + + return photo, nil +} + +// FindPhotoByUUID returns a Photo based on the UUID. +func (s *Repo) FindPhotoByUUID(photoUUID string) (photo models.Photo, err error) { + if err := s.db.Where("photo_uuid = ?", photoUUID).First(&photo).Error; err != nil { + return photo, err + } + + return photo, nil +} diff --git a/internal/photoprism/search_test.go b/internal/repo/photos_test.go similarity index 98% rename from internal/photoprism/search_test.go rename to internal/repo/photos_test.go index a4359fc5a..f7060a496 100644 --- a/internal/photoprism/search_test.go +++ b/internal/repo/photos_test.go @@ -1,4 +1,4 @@ -package photoprism +package repo import ( "github.com/stretchr/testify/assert" @@ -13,7 +13,7 @@ func TestSearch_Photos_Query(t *testing.T) { conf.CreateDirectories() - search := NewSearch(conf.OriginalsPath(), conf.Db()) + search := New(conf.OriginalsPath(), conf.Db()) t.Run("normal query", func(t *testing.T) { var f form.PhotoSearch diff --git a/internal/repo/repo.go b/internal/repo/repo.go new file mode 100644 index 000000000..d06da6878 --- /dev/null +++ b/internal/repo/repo.go @@ -0,0 +1,40 @@ +/* +This package contains PhotoPrism database queries. + +Additional information can be found in our Developer Guide: + +https://github.com/photoprism/photoprism/wiki +*/ +package repo + +import ( + "github.com/photoprism/photoprism/internal/event" + + "github.com/jinzhu/gorm" +) + +var log = event.Log + +// About 1km ('good enough' for now) +const SearchRadius = 0.009 + +// Repo searches given an originals path and a db instance. +type Repo struct { + originalsPath string + db *gorm.DB +} + +// SearchCount is the total number of search hits. +type SearchCount struct { + Total int +} + +// New returns a new Repo type with a given path and db instance. +func New(originalsPath string, db *gorm.DB) *Repo { + instance := &Repo{ + originalsPath: originalsPath, + db: db, + } + + return instance +}