Backend: Move SQL queries to repo package

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer
2019-12-11 07:37:39 +01:00
parent 458a2afbd4
commit d4b3e456f7
15 changed files with 688 additions and 659 deletions

View File

@@ -13,11 +13,11 @@ import (
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/models" "github.com/photoprism/photoprism/internal/models"
"github.com/photoprism/photoprism/internal/repo"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/util" "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) { router.GET("/albums", func(c *gin.Context) {
var f form.AlbumSearch var f form.AlbumSearch
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
err := c.MustBindWith(&f, binding.Form) err := c.MustBindWith(&f, binding.Form)
if err != nil { if err != nil {
@@ -34,7 +34,7 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
return return
} }
result, err := search.Albums(f) result, err := r.Albums(f)
if err != nil { if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())}) c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())})
return return
@@ -51,8 +51,8 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
func GetAlbum(router *gin.RouterGroup, conf *config.Config) { func GetAlbum(router *gin.RouterGroup, conf *config.Config) {
router.GET("/albums/:uuid", func(c *gin.Context) { router.GET("/albums/:uuid", func(c *gin.Context) {
id := c.Param("uuid") 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 { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) 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") 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 { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) 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") 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 { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
@@ -169,9 +169,9 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
return 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 { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
@@ -198,9 +198,8 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
return return
} }
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
album, err := r.FindAlbumByUUID(c.Param("uuid"))
album, err := search.FindAlbumByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
@@ -237,8 +236,8 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
return return
} }
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
a, err := search.FindAlbumByUUID(c.Param("uuid")) a, err := r.FindAlbumByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) 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 var failed []string
for _, photoUUID := range f.Photos { 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) failed = append(failed, photoUUID)
} else { } else {
added = append(added, models.NewPhotoAlbum(p.PhotoUUID, a.AlbumUUID).FirstOrCreate(db)) added = append(added, models.NewPhotoAlbum(p.PhotoUUID, a.AlbumUUID).FirstOrCreate(db))
@@ -288,8 +287,8 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
return return
} }
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
a, err := search.FindAlbumByUUID(c.Param("uuid")) a, err := r.FindAlbumByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) 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() start := time.Now()
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
a, err := search.FindAlbumByUUID(c.Param("uuid")) a, err := r.FindAlbumByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
return return
} }
p, err := search.Photos(form.PhotoSearch{ p, err := r.Photos(form.PhotoSearch{
Album: a.AlbumUUID, Album: a.AlbumUUID,
Count: 10000, Count: 10000,
Offset: 0, Offset: 0,

View File

@@ -4,10 +4,10 @@ import (
"fmt" "fmt"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/repo"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism"
) )
// TODO: GET /api/v1/dl/file/:hash // 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) { router.GET("/download/:hash", func(c *gin.Context) {
fileHash := c.Param("hash") fileHash := c.Param("hash")
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
file, err := search.FindFileByHash(fileHash) file, err := r.FindFileByHash(fileHash)
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})

View File

@@ -8,7 +8,7 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/repo"
"github.com/photoprism/photoprism/internal/util" "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) { router.GET("/labels", func(c *gin.Context) {
var f form.LabelSearch var f form.LabelSearch
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
err := c.MustBindWith(&f, binding.Form) err := c.MustBindWith(&f, binding.Form)
if err != nil { if err != nil {
@@ -25,7 +25,7 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
return return
} }
result, err := search.Labels(f) result, err := r.Labels(f)
if err != nil { if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())}) c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())})
return return
@@ -49,9 +49,9 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
return 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 { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
@@ -76,9 +76,9 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
return 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 { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})

View File

@@ -7,12 +7,12 @@ import (
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/repo"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
) )
// GET /api/v1/photos // GET /api/v1/photos
@@ -33,7 +33,7 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
router.GET("/photos", func(c *gin.Context) { router.GET("/photos", func(c *gin.Context) {
var f form.PhotoSearch var f form.PhotoSearch
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
err := c.MustBindWith(&f, binding.Form) err := c.MustBindWith(&f, binding.Form)
if err != nil { if err != nil {
@@ -41,7 +41,7 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
return return
} }
result, err := search.Photos(f) result, err := r.Photos(f)
if err != nil { if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())}) 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 // uuid: string PhotoUUID as returned by the API
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) { func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
router.GET("/photos/:uuid/download", func(c *gin.Context) { router.GET("/photos/:uuid/download", func(c *gin.Context) {
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
file, err := search.FindFileByPhotoUUID(c.Param("uuid")) file, err := r.FindFileByPhotoUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
@@ -100,8 +100,8 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
return return
} }
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
m, err := search.FindPhotoByUUID(c.Param("uuid")) m, err := r.FindPhotoByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
@@ -130,8 +130,8 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
return return
} }
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
m, err := search.FindPhotoByUUID(c.Param("uuid")) m, err := r.FindPhotoByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})

View File

@@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/repo"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -29,8 +30,8 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
return return
} }
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
file, err := search.FindFileByHash(fileHash) file, err := r.FindFileByHash(fileHash)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()})
@@ -83,11 +84,11 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
return 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")) // 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) // log.Infof("Label thumb file: %#v", file)
@@ -138,9 +139,9 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
return 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 { if err != nil {
log.Debugf("album has no photos yet, using generic thumb image: %s", uuid) log.Debugf("album has no photos yet, using generic thumb image: %s", uuid)

View File

@@ -12,10 +12,10 @@ import (
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/repo"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism"
) )
// POST /api/v1/zip // POST /api/v1/zip
@@ -35,8 +35,8 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
return return
} }
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) r := repo.New(conf.OriginalsPath(), conf.Db())
files, err := search.FindFilesByUUID(f.Photos, 1000, 0) files, err := r.FindFilesByUUID(f.Photos, 1000, 0)
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})

View File

@@ -19,13 +19,13 @@ type Indexer struct {
// NewIndexer returns a new indexer. // NewIndexer returns a new indexer.
// TODO: Is it really necessary to return a pointer? // TODO: Is it really necessary to return a pointer?
func NewIndexer(conf *config.Config, tensorFlow *TensorFlow) *Indexer { func NewIndexer(conf *config.Config, tensorFlow *TensorFlow) *Indexer {
instance := &Indexer{ i := &Indexer{
conf: conf, conf: conf,
tensorFlow: tensorFlow, tensorFlow: tensorFlow,
db: conf.Db(), db: conf.Db(),
} }
return instance return i
} }
func (i *Indexer) originalsPath() string { func (i *Indexer) originalsPath() string {

View File

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

View File

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

96
internal/repo/albums.go Normal file
View File

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

49
internal/repo/files.go Normal file
View File

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

125
internal/repo/labels.go Normal file
View File

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

324
internal/repo/photos.go Normal file
View File

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

View File

@@ -1,4 +1,4 @@
package photoprism package repo
import ( import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -13,7 +13,7 @@ func TestSearch_Photos_Query(t *testing.T) {
conf.CreateDirectories() conf.CreateDirectories()
search := NewSearch(conf.OriginalsPath(), conf.Db()) search := New(conf.OriginalsPath(), conf.Db())
t.Run("normal query", func(t *testing.T) { t.Run("normal query", func(t *testing.T) {
var f form.PhotoSearch var f form.PhotoSearch

40
internal/repo/repo.go Normal file
View File

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