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/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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 (
"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

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
}