Backend: Query package refactoring

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer
2020-05-08 15:41:01 +02:00
parent 695294fc58
commit 842da9f09b
66 changed files with 1153 additions and 1229 deletions

View File

@@ -10,7 +10,7 @@ import (
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"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/service" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/workers" "github.com/photoprism/photoprism/internal/workers"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
@@ -25,7 +25,6 @@ func GetAccounts(router *gin.RouterGroup, conf *config.Config) {
var f form.AccountSearch var f form.AccountSearch
q := service.Query()
err := c.MustBindWith(&f, binding.Form) err := c.MustBindWith(&f, binding.Form)
if err != nil { if err != nil {
@@ -33,7 +32,7 @@ func GetAccounts(router *gin.RouterGroup, conf *config.Config) {
return return
} }
result, err := q.Accounts(f) result, err := query.Accounts(f)
if err != nil { if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
@@ -59,10 +58,9 @@ func GetAccount(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query()
id := ParseUint(c.Param("id")) id := ParseUint(c.Param("id"))
if m, err := q.AccountByID(id); err == nil { if m, err := query.AccountByID(id); err == nil {
c.JSON(http.StatusOK, m) c.JSON(http.StatusOK, m)
} else { } else {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
@@ -81,10 +79,9 @@ func GetAccountDirs(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query()
id := ParseUint(c.Param("id")) id := ParseUint(c.Param("id"))
m, err := q.AccountByID(id) m, err := query.AccountByID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
@@ -114,10 +111,9 @@ func ShareWithAccount(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query()
id := ParseUint(c.Param("id")) id := ParseUint(c.Param("id"))
m, err := q.AccountByID(id) m, err := query.AccountByID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
@@ -132,7 +128,7 @@ func ShareWithAccount(router *gin.RouterGroup, conf *config.Config) {
} }
dst := f.Destination dst := f.Destination
files, err := q.FilesByUUID(f.Photos, 1000, 0) files, err := query.FilesByUUID(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()})
@@ -202,9 +198,7 @@ func UpdateAccount(router *gin.RouterGroup, conf *config.Config) {
id := ParseUint(c.Param("id")) id := ParseUint(c.Param("id"))
q := service.Query() m, err := query.AccountByID(id)
m, err := q.AccountByID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -236,7 +230,7 @@ func UpdateAccount(router *gin.RouterGroup, conf *config.Config) {
event.Success("account saved") event.Success("account saved")
m, err = q.AccountByID(id) m, err = query.AccountByID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
@@ -259,9 +253,8 @@ func DeleteAccount(router *gin.RouterGroup, conf *config.Config) {
} }
id := ParseUint(c.Param("id")) id := ParseUint(c.Param("id"))
q := service.Query()
m, err := q.AccountByID(id) m, err := query.AccountByID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)

View File

@@ -14,7 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"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/service" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb" "github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
@@ -35,7 +35,6 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
var f form.AlbumSearch var f form.AlbumSearch
q := service.Query()
err := c.MustBindWith(&f, binding.Form) err := c.MustBindWith(&f, binding.Form)
if err != nil { if err != nil {
@@ -43,7 +42,7 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
return return
} }
result, err := q.Albums(f) result, err := query.Albums(f)
if err != nil { if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
return return
@@ -61,8 +60,7 @@ 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")
q := service.Query() m, err := query.AlbumByUUID(id)
m, err := q.AlbumByUUID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@@ -88,13 +86,12 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query()
m := entity.NewAlbum(f.AlbumName) m := entity.NewAlbum(f.AlbumName)
m.AlbumFavorite = f.AlbumFavorite m.AlbumFavorite = f.AlbumFavorite
log.Debugf("create album: %+v %+v", f, m) log.Debugf("create album: %+v %+v", f, m)
if res := conf.Db().Create(m); res.Error != nil { if res := entity.Db().Create(m); res.Error != nil {
log.Error(res.Error.Error()) log.Error(res.Error.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("%s already exists", txt.Quote(m.AlbumName))}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("%s already exists", txt.Quote(m.AlbumName))})
return return
@@ -104,7 +101,7 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
event.Publish("config.updated", event.Data(conf.ClientConfig())) event.Publish("config.updated", event.Data(conf.ClientConfig()))
PublishAlbumEvent(EntityCreated, m.AlbumUUID, c, q) PublishAlbumEvent(EntityCreated, m.AlbumUUID, c)
c.JSON(http.StatusOK, m) c.JSON(http.StatusOK, m)
}) })
@@ -119,9 +116,7 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
} }
uuid := c.Param("uuid") uuid := c.Param("uuid")
q := service.Query() m, err := query.AlbumByUUID(uuid)
m, err := q.AlbumByUUID(uuid)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@@ -151,7 +146,7 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
event.Publish("config.updated", event.Data(conf.ClientConfig())) event.Publish("config.updated", event.Data(conf.ClientConfig()))
event.Success("album saved") event.Success("album saved")
PublishAlbumEvent(EntityUpdated, uuid, c, q) PublishAlbumEvent(EntityUpdated, uuid, c)
c.JSON(http.StatusOK, m) c.JSON(http.StatusOK, m)
}) })
@@ -166,16 +161,15 @@ func DeleteAlbum(router *gin.RouterGroup, conf *config.Config) {
} }
id := c.Param("uuid") id := c.Param("uuid")
q := service.Query()
m, err := q.AlbumByUUID(id) m, err := query.AlbumByUUID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
return return
} }
PublishAlbumEvent(EntityDeleted, id, c, q) PublishAlbumEvent(EntityDeleted, id, c)
conf.Db().Delete(&m) conf.Db().Delete(&m)
@@ -198,9 +192,7 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
} }
id := c.Param("uuid") id := c.Param("uuid")
q := service.Query() album, err := query.AlbumByUUID(id)
album, err := q.AlbumByUUID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@@ -211,7 +203,7 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
conf.Db().Save(&album) conf.Db().Save(&album)
event.Publish("config.updated", event.Data(conf.ClientConfig())) event.Publish("config.updated", event.Data(conf.ClientConfig()))
PublishAlbumEvent(EntityUpdated, id, c, q) PublishAlbumEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{}) c.JSON(http.StatusOK, http.Response{})
}) })
@@ -229,8 +221,7 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
} }
id := c.Param("uuid") id := c.Param("uuid")
q := service.Query() album, err := query.AlbumByUUID(id)
album, err := q.AlbumByUUID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@@ -241,7 +232,7 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
conf.Db().Save(&album) conf.Db().Save(&album)
event.Publish("config.updated", event.Data(conf.ClientConfig())) event.Publish("config.updated", event.Data(conf.ClientConfig()))
PublishAlbumEvent(EntityUpdated, id, c, q) PublishAlbumEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{}) c.JSON(http.StatusOK, http.Response{})
}) })
@@ -263,15 +254,14 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
} }
uuid := c.Param("uuid") uuid := c.Param("uuid")
q := service.Query() a, err := query.AlbumByUUID(uuid)
a, err := q.AlbumByUUID(uuid)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
return return
} }
photos, err := q.PhotoSelection(f) photos, err := query.PhotoSelection(f)
if err != nil { if err != nil {
log.Errorf("album: %s", err) log.Errorf("album: %s", err)
@@ -291,7 +281,7 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
event.Success(fmt.Sprintf("%d photos added to %s", len(added), a.AlbumName)) event.Success(fmt.Sprintf("%d photos added to %s", len(added), a.AlbumName))
} }
PublishAlbumEvent(EntityUpdated, a.AlbumUUID, c, q) PublishAlbumEvent(EntityUpdated, a.AlbumUUID, c)
c.JSON(http.StatusOK, gin.H{"message": "photos added to album", "album": a, "added": added}) c.JSON(http.StatusOK, gin.H{"message": "photos added to album", "album": a, "added": added})
}) })
@@ -318,21 +308,18 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query() a, err := query.AlbumByUUID(c.Param("uuid"))
a, err := q.AlbumByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
return return
} }
db := conf.Db() entity.Db().Where("album_uuid = ? AND photo_uuid IN (?)", a.AlbumUUID, f.Photos).Delete(&entity.PhotoAlbum{})
db.Where("album_uuid = ? AND photo_uuid IN (?)", a.AlbumUUID, f.Photos).Delete(&entity.PhotoAlbum{})
event.Success(fmt.Sprintf("photos removed from %s", a.AlbumName)) event.Success(fmt.Sprintf("photos removed from %s", a.AlbumName))
PublishAlbumEvent(EntityUpdated, a.AlbumUUID, c, q) PublishAlbumEvent(EntityUpdated, a.AlbumUUID, c)
c.JSON(http.StatusOK, gin.H{"message": "photos removed from album", "album": a, "photos": f.Photos}) c.JSON(http.StatusOK, gin.H{"message": "photos removed from album", "album": a, "photos": f.Photos})
}) })
@@ -343,15 +330,14 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
router.GET("/albums/:uuid/download", func(c *gin.Context) { router.GET("/albums/:uuid/download", func(c *gin.Context) {
start := time.Now() start := time.Now()
q := service.Query() a, err := query.AlbumByUUID(c.Param("uuid"))
a, err := q.AlbumByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
return return
} }
p, _, err := q.Photos(form.PhotoSearch{ p, _, err := query.Photos(form.PhotoSearch{
Album: a.AlbumUUID, Album: a.AlbumUUID,
Count: 10000, Count: 10000,
Offset: 0, Offset: 0,
@@ -384,7 +370,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
defer newZipFile.Close() defer newZipFile.Close()
zipWriter := zip.NewWriter(newZipFile) zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close() defer func() { _ = zipWriter.Close() }()
for _, f := range p { for _, f := range p {
fileName := path.Join(conf.OriginalsPath(), f.FileName) fileName := path.Join(conf.OriginalsPath(), f.FileName)
@@ -405,7 +391,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
} }
log.Infof("album: archive %s created in %s", txt.Quote(zipBaseName), time.Since(start)) log.Infof("album: archive %s created in %s", txt.Quote(zipBaseName), time.Since(start))
zipWriter.Close() _ = zipWriter.Close()
newZipFile.Close() newZipFile.Close()
if !fs.FileExists(zipFileName) { if !fs.FileExists(zipFileName) {
@@ -443,8 +429,6 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query()
gc := conf.Cache() gc := conf.Cache()
cacheKey := fmt.Sprintf("album-thumbnail:%s:%s", uuid, typeName) cacheKey := fmt.Sprintf("album-thumbnail:%s:%s", uuid, typeName)
@@ -454,7 +438,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
return return
} }
f, err := q.AlbumThumbByUUID(uuid) f, err := query.AlbumThumbByUUID(uuid)
if err != nil { if err != nil {
log.Debugf("album: no photos yet, using generic image for %s", uuid) log.Debugf("album: no photos yet, using generic image for %s", uuid)
@@ -470,7 +454,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
// Set missing flag so that the file doesn't show up in search results anymore // Set missing flag so that the file doesn't show up in search results anymore
f.FileMissing = true f.FileMissing = true
conf.Db().Save(&f) entity.Db().Save(&f)
return return
} }

View File

@@ -41,9 +41,11 @@ func BatchPhotosArchive(router *gin.RouterGroup, conf *config.Config) {
log.Infof("photos: archiving %#v", f.Photos) log.Infof("photos: archiving %#v", f.Photos)
db := conf.Db() entity.Db().Where("photo_uuid IN (?)", f.Photos).Delete(&entity.Photo{})
db.Where("photo_uuid IN (?)", f.Photos).Delete(&entity.Photo{}) if err := query.UpdatePhotoCounts(); err != nil {
log.Errorf("photos: %s", err)
}
elapsed := int(time.Since(start).Seconds()) elapsed := int(time.Since(start).Seconds())
@@ -80,9 +82,7 @@ func BatchPhotosRestore(router *gin.RouterGroup, conf *config.Config) {
log.Infof("restoring photos: %#v", f.Photos) log.Infof("restoring photos: %#v", f.Photos)
db := conf.Db() entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).
db.Unscoped().Model(&entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).
UpdateColumn("deleted_at", gorm.Expr("NULL")) UpdateColumn("deleted_at", gorm.Expr("NULL"))
elapsed := int(time.Since(start).Seconds()) elapsed := int(time.Since(start).Seconds())
@@ -118,10 +118,8 @@ func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
log.Infof("albums: deleting %#v", f.Albums) log.Infof("albums: deleting %#v", f.Albums)
db := conf.Db() entity.Db().Where("album_uuid IN (?)", f.Albums).Delete(&entity.Album{})
entity.Db().Where("album_uuid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
db.Where("album_uuid IN (?)", f.Albums).Delete(&entity.Album{})
db.Where("album_uuid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
event.Publish("config.updated", event.Data(conf.ClientConfig())) event.Publish("config.updated", event.Data(conf.ClientConfig()))
@@ -156,18 +154,14 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
log.Infof("marking photos as private: %#v", f.Photos) log.Infof("marking photos as private: %#v", f.Photos)
db := conf.Db() err := entity.Db().Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")).Error
err := db.Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")).Error
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed)
return return
} }
q := query.New(db) if entities, err := query.PhotoSelection(f); err == nil {
if entities, err := q.PhotoSelection(f); err == nil {
event.EntitiesUpdated("photos", entities) event.EntitiesUpdated("photos", entities)
} }
@@ -204,9 +198,7 @@ func BatchPhotosStory(router *gin.RouterGroup, conf *config.Config) {
log.Infof("marking photos as story: %#v", f.Photos) log.Infof("marking photos as story: %#v", f.Photos)
db := conf.Db() entity.Db().Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).Updates(map[string]interface{}{
db.Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).Updates(map[string]interface{}{
"photo_story": gorm.Expr("IF (`photo_story`, 0, 1)"), "photo_story": gorm.Expr("IF (`photo_story`, 0, 1)"),
}) })
@@ -239,9 +231,7 @@ func BatchLabelsDelete(router *gin.RouterGroup, conf *config.Config) {
log.Infof("labels: deleting %#v", f.Labels) log.Infof("labels: deleting %#v", f.Labels)
db := conf.Db() entity.Db().Where("label_uuid IN (?)", f.Labels).Delete(&entity.Label{})
db.Where("label_uuid IN (?)", f.Labels).Delete(&entity.Label{})
event.Publish("config.updated", event.Data(conf.ClientConfig())) event.Publish("config.updated", event.Data(conf.ClientConfig()))

View File

@@ -5,7 +5,8 @@ import (
"path" "path"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -23,8 +24,7 @@ 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")
q := service.Query() f, err := query.FileByHash(fileHash)
f, err := q.FileByHash(fileHash)
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
@@ -39,7 +39,7 @@ func GetDownload(router *gin.RouterGroup, conf *config.Config) {
// Set missing flag so that the file doesn't show up in search results anymore // Set missing flag so that the file doesn't show up in search results anymore
f.FileMissing = true f.FileMissing = true
conf.Db().Save(&f) entity.Db().Save(&f)
return return
} }

View File

@@ -17,9 +17,9 @@ const (
EntityDeleted EntityEvent = "deleted" EntityDeleted EntityEvent = "deleted"
) )
func PublishPhotoEvent(e EntityEvent, uuid string, c *gin.Context, q *query.Query) { func PublishPhotoEvent(e EntityEvent, uuid string, c *gin.Context) {
f := form.PhotoSearch{ID: uuid, Merged: true} f := form.PhotoSearch{ID: uuid, Merged: true}
result, _, err := q.Photos(f) result, _, err := query.Photos(f)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
@@ -30,9 +30,9 @@ func PublishPhotoEvent(e EntityEvent, uuid string, c *gin.Context, q *query.Quer
event.PublishEntities("photos", string(e), result) event.PublishEntities("photos", string(e), result)
} }
func PublishAlbumEvent(e EntityEvent, uuid string, c *gin.Context, q *query.Query) { func PublishAlbumEvent(e EntityEvent, uuid string, c *gin.Context) {
f := form.AlbumSearch{ID: uuid} f := form.AlbumSearch{ID: uuid}
result, err := q.Albums(f) result, err := query.Albums(f)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
@@ -43,9 +43,9 @@ func PublishAlbumEvent(e EntityEvent, uuid string, c *gin.Context, q *query.Quer
event.PublishEntities("albums", string(e), result) event.PublishEntities("albums", string(e), result)
} }
func PublishLabelEvent(e EntityEvent, uuid string, c *gin.Context, q *query.Query) { func PublishLabelEvent(e EntityEvent, uuid string, c *gin.Context) {
f := form.LabelSearch{ID: uuid} f := form.LabelSearch{ID: uuid}
result, err := q.Labels(f) result, err := query.Labels(f)
if err != nil { if err != nil {
log.Error(err) log.Error(err)

View File

@@ -5,9 +5,9 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
@@ -22,8 +22,7 @@ func GetFile(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query() p, err := query.FileByHash(c.Param("hash"))
p, err := q.FileByHash(c.Param("hash"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -45,10 +44,7 @@ func LinkFile(router *gin.RouterGroup, conf *config.Config) {
return return
} }
db := conf.Db() m, err := query.FileByUUID(c.Param("uuid"))
q := query.New(db)
m, err := q.FileByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrFileNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrFileNotFound)
@@ -59,7 +55,7 @@ func LinkFile(router *gin.RouterGroup, conf *config.Config) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return return
} else { } else {
db.Model(&m).Association("Links").Append(link) entity.Db().Model(&m).Association("Links").Append(link)
} }
event.Success("created file share link") event.Success("created file share link")

View File

@@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -24,7 +24,6 @@ func GetGeo(router *gin.RouterGroup, conf *config.Config) {
var f form.GeoSearch var f form.GeoSearch
q := service.Query()
err := c.MustBindWith(&f, binding.Form) err := c.MustBindWith(&f, binding.Form)
if err != nil { if err != nil {
@@ -32,7 +31,7 @@ func GetGeo(router *gin.RouterGroup, conf *config.Config) {
return return
} }
photos, err := q.Geo(f) photos, err := query.Geo(f)
if err != nil { if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})

View File

@@ -11,9 +11,10 @@ import (
"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/entity"
"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/service" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb" "github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
@@ -29,7 +30,6 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
var f form.LabelSearch var f form.LabelSearch
q := service.Query()
err := c.MustBindWith(&f, binding.Form) err := c.MustBindWith(&f, binding.Form)
if err != nil { if err != nil {
@@ -37,7 +37,8 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
return return
} }
result, err := q.Labels(f) result, err := query.Labels(f)
if err != nil { if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
return return
@@ -67,9 +68,7 @@ func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
} }
id := c.Param("uuid") id := c.Param("uuid")
q := service.Query() m, err := query.LabelByUUID(id)
m, err := q.LabelByUUID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
@@ -77,11 +76,11 @@ func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
} }
m.SetName(f.LabelName) m.SetName(f.LabelName)
conf.Db().Save(&m) entity.Db().Save(&m)
event.Success("label saved") event.Success("label saved")
PublishLabelEvent(EntityUpdated, id, c, q) PublishLabelEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, m) c.JSON(http.StatusOK, m)
}) })
@@ -99,9 +98,7 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
} }
id := c.Param("uuid") id := c.Param("uuid")
q := service.Query() label, err := query.LabelByUUID(id)
label, err := q.LabelByUUID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(404, gin.H{"error": txt.UcFirst(err.Error())})
@@ -109,7 +106,7 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
} }
label.LabelFavorite = true label.LabelFavorite = true
conf.Db().Save(&label) entity.Db().Save(&label)
if label.LabelPriority < 0 { if label.LabelPriority < 0 {
event.Publish("count.labels", event.Data{ event.Publish("count.labels", event.Data{
@@ -117,7 +114,7 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
}) })
} }
PublishLabelEvent(EntityUpdated, id, c, q) PublishLabelEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{}) c.JSON(http.StatusOK, http.Response{})
}) })
@@ -135,9 +132,7 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
} }
id := c.Param("uuid") id := c.Param("uuid")
q := service.Query() label, err := query.LabelByUUID(id)
label, err := q.LabelByUUID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(404, gin.H{"error": txt.UcFirst(err.Error())})
@@ -145,7 +140,7 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
} }
label.LabelFavorite = false label.LabelFavorite = false
conf.Db().Save(&label) entity.Db().Save(&label)
if label.LabelPriority < 0 { if label.LabelPriority < 0 {
event.Publish("count.labels", event.Data{ event.Publish("count.labels", event.Data{
@@ -153,7 +148,7 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
}) })
} }
PublishLabelEvent(EntityUpdated, id, c, q) PublishLabelEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{}) c.JSON(http.StatusOK, http.Response{})
}) })
@@ -180,8 +175,6 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query()
gc := conf.Cache() gc := conf.Cache()
cacheKey := fmt.Sprintf("label-thumbnail:%s:%s", labelUUID, typeName) cacheKey := fmt.Sprintf("label-thumbnail:%s:%s", labelUUID, typeName)
@@ -191,7 +184,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
return return
} }
f, err := q.LabelThumbByUUID(labelUUID) f, err := query.LabelThumbByUUID(labelUUID)
if err != nil { if err != nil {
log.Errorf(err.Error()) log.Errorf(err.Error())

View File

@@ -39,10 +39,7 @@ func LinkAlbum(router *gin.RouterGroup, conf *config.Config) {
return return
} }
db := conf.Db() m, err := query.AlbumByUUID(c.Param("uuid"))
q := query.New(db)
m, err := q.AlbumByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@@ -53,7 +50,7 @@ func LinkAlbum(router *gin.RouterGroup, conf *config.Config) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return return
} else { } else {
db.Model(&m).Association("Links").Append(link) entity.Db().Model(&m).Association("Links").Append(link)
} }
event.Success("created album share link") event.Success("created album share link")
@@ -70,10 +67,7 @@ func LinkPhoto(router *gin.RouterGroup, conf *config.Config) {
return return
} }
db := conf.Db() m, err := query.PhotoByUUID(c.Param("uuid"))
q := query.New(db)
m, err := q.PhotoByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -84,7 +78,7 @@ func LinkPhoto(router *gin.RouterGroup, conf *config.Config) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return return
} else { } else {
db.Model(&m).Association("Links").Append(link) entity.Db().Model(&m).Association("Links").Append(link)
} }
event.Success("created photo share link") event.Success("created photo share link")
@@ -101,10 +95,7 @@ func LinkLabel(router *gin.RouterGroup, conf *config.Config) {
return return
} }
db := conf.Db() m, err := query.LabelByUUID(c.Param("uuid"))
q := query.New(db)
m, err := q.LabelByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
@@ -115,7 +106,7 @@ func LinkLabel(router *gin.RouterGroup, conf *config.Config) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return return
} else { } else {
db.Model(&m).Association("Links").Append(link) entity.Db().Model(&m).Association("Links").Append(link)
} }
event.Success("created label share link") event.Success("created label share link")

View File

@@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -18,9 +18,8 @@ func GetMomentsTime(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query() result, err := query.GetMomentsTime()
result, err := q.GetMomentsTime()
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
return return

View File

@@ -11,7 +11,6 @@ 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/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
) )
@@ -26,8 +25,7 @@ func GetPhoto(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query() p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
p, err := q.PreloadPhotoByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -46,11 +44,8 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
return return
} }
db := conf.Db()
uuid := c.Param("uuid") uuid := c.Param("uuid")
q := query.New(db) m, err := query.PhotoByUUID(uuid)
m, err := q.PhotoByUUID(uuid)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -81,11 +76,15 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
return return
} }
PublishPhotoEvent(EntityUpdated, uuid, c, q) if err := query.UpdatePhotoCounts(); err != nil {
log.Errorf("photo: %s", err)
}
PublishPhotoEvent(EntityUpdated, uuid, c)
event.Success("photo saved") event.Success("photo saved")
p, err := q.PreloadPhotoByUUID(uuid) p, err := query.PreloadPhotoByUUID(uuid)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -102,8 +101,7 @@ func UpdatePhoto(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) {
q := service.Query() f, err := query.FileByPhotoUUID(c.Param("uuid"))
f, err := q.FileByPhotoUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -142,8 +140,7 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
} }
id := c.Param("uuid") id := c.Param("uuid")
q := service.Query() m, err := query.PhotoByUUID(id)
m, err := q.PhotoByUUID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -158,7 +155,7 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
"count": 1, "count": 1,
}) })
PublishPhotoEvent(EntityUpdated, id, c, q) PublishPhotoEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, gin.H{"photo": m}) c.JSON(http.StatusOK, gin.H{"photo": m})
}) })
@@ -176,8 +173,7 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
} }
id := c.Param("uuid") id := c.Param("uuid")
q := service.Query() m, err := query.PhotoByUUID(id)
m, err := q.PhotoByUUID(id)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -186,13 +182,13 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
m.PhotoFavorite = false m.PhotoFavorite = false
m.PhotoQuality = m.QualityScore() m.PhotoQuality = m.QualityScore()
conf.Db().Save(&m) entity.Db().Save(&m)
event.Publish("count.favorites", event.Data{ event.Publish("count.favorites", event.Data{
"count": -1, "count": -1,
}) })
PublishPhotoEvent(EntityUpdated, id, c, q) PublishPhotoEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, gin.H{"photo": m}) c.JSON(http.StatusOK, gin.H{"photo": m})
}) })
@@ -209,23 +205,20 @@ func SetPhotoPrimary(router *gin.RouterGroup, conf *config.Config) {
return return
} }
db := conf.Db()
uuid := c.Param("uuid") uuid := c.Param("uuid")
fileUUID := c.Param("file_uuid") fileUUID := c.Param("file_uuid")
q := query.New(db) err := query.SetPhotoPrimary(uuid, fileUUID)
err := q.SetPhotoPrimary(uuid, fileUUID)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
return return
} }
PublishPhotoEvent(EntityUpdated, uuid, c, q) PublishPhotoEvent(EntityUpdated, uuid, c)
event.Success("photo saved") event.Success("photo saved")
p, err := q.PreloadPhotoByUUID(uuid) p, err := query.PreloadPhotoByUUID(uuid)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)

View File

@@ -10,7 +10,6 @@ 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/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
@@ -25,9 +24,7 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query() m, err := query.PhotoByUUID(c.Param("uuid"))
m, err := q.PhotoByUUID(c.Param("uuid"))
db := conf.Db()
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -55,14 +52,14 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
plm.Uncertainty = f.Uncertainty plm.Uncertainty = f.Uncertainty
plm.LabelSrc = entity.SrcManual plm.LabelSrc = entity.SrcManual
if err := db.Save(&plm).Error; err != nil { if err := entity.Db().Save(&plm).Error; err != nil {
log.Errorf("label: %s", err) log.Errorf("label: %s", err)
} }
} }
db.Save(&lm) entity.Db().Save(&lm)
p, err := q.PreloadPhotoByUUID(c.Param("uuid")) p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -74,7 +71,7 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return return
} }
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c, q) PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
event.Success("label updated") event.Success("label updated")
@@ -94,9 +91,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return return
} }
db := conf.Db() m, err := query.PhotoByUUID(c.Param("uuid"))
q := query.New(db)
m, err := q.PhotoByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -110,7 +105,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return return
} }
label, err := q.PhotoLabel(m.ID, uint(labelId)) label, err := query.PhotoLabel(m.ID, uint(labelId))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
@@ -118,13 +113,13 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
} }
if label.LabelSrc == entity.SrcManual { if label.LabelSrc == entity.SrcManual {
db.Delete(&label) entity.Db().Delete(&label)
} else { } else {
label.Uncertainty = 100 label.Uncertainty = 100
db.Save(&label) entity.Db().Save(&label)
} }
p, err := q.PreloadPhotoByUUID(c.Param("uuid")) p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -136,7 +131,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return return
} }
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c, q) PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
event.Success("label removed") event.Success("label removed")
@@ -158,9 +153,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
// TODO: Code clean-up, simplify // TODO: Code clean-up, simplify
db := conf.Db() m, err := query.PhotoByUUID(c.Param("uuid"))
q := query.New(db)
m, err := q.PhotoByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -174,7 +167,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return return
} }
label, err := q.PhotoLabel(m.ID, uint(labelId)) label, err := query.PhotoLabel(m.ID, uint(labelId))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
@@ -191,7 +184,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return return
} }
p, err := q.PreloadPhotoByUUID(c.Param("uuid")) p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -203,7 +196,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return return
} }
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c, q) PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
event.Success("label saved") event.Success("label saved")

View File

@@ -5,7 +5,7 @@ import (
"strconv" "strconv"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -36,7 +36,6 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
var f form.PhotoSearch var f form.PhotoSearch
q := service.Query()
err := c.MustBindWith(&f, binding.Form) err := c.MustBindWith(&f, binding.Form)
if err != nil { if err != nil {
@@ -44,7 +43,7 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
return return
} }
result, count, err := q.Photos(f) result, count, err := query.Photos(f)
if err != nil { if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())}) c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})

View File

@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb" "github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
@@ -31,9 +32,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
return return
} }
db := conf.Db() f, err := query.FileByHash(fileHash)
q := query.New(db)
f, err := q.FileByHash(fileHash)
if err != nil { if err != nil {
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg) c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
@@ -53,7 +52,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
// Set missing flag so that the file doesn't show up in search results anymore // Set missing flag so that the file doesn't show up in search results anymore
f.FileMissing = true f.FileMissing = true
db.Save(&f) entity.Db().Save(&f)
return return
} }

View File

@@ -13,7 +13,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"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/service" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb" "github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
) )
@@ -45,8 +45,7 @@ func GetPreview(router *gin.RouterGroup, conf *config.Config) {
f.Count = 12 f.Count = 12
f.Order = "relevance" f.Order = "relevance"
q := service.Query() p, _, err := query.Photos(f)
p, _, err := q.Photos(f)
if err != nil { if err != nil {
log.Error(err) log.Error(err)

View File

@@ -12,7 +12,7 @@ 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/service" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
@@ -47,8 +47,7 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
return return
} }
q := service.Query() files, err := query.FilesByUUID(f.Photos, 1000, 0)
files, err := q.FilesByUUID(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

@@ -162,7 +162,7 @@ func (c *Config) ClientConfig() ClientConfig {
Take(&count) Take(&count)
db.Table("places"). db.Table("places").
Select("(COUNT(*) - 1) AS places"). Select("(SUM(photo_count > 0) - 1) AS places").
Take(&count) Take(&count)
type country struct { type country struct {

View File

@@ -62,9 +62,9 @@ func (c *Config) InitDb() {
} }
// ResetDb drops all tables in the currently configured database and re-creates them. // ResetDb drops all tables in the currently configured database and re-creates them.
func (c *Config) ResetDb(testFixtures bool) { func (c *Config) ResetDb() {
entity.SetDbProvider(c) entity.SetDbProvider(c)
entity.ResetDb(testFixtures) entity.InitTestFixtures()
} }
// connectToDatabase establishes a database connection. // connectToDatabase establishes a database connection.

View File

@@ -102,7 +102,7 @@ func NewTestConfig() *Config {
log.Fatalf("config: %s", err.Error()) log.Fatalf("config: %s", err.Error())
} }
c.ResetDb(true) c.ResetDb()
thumb.Size = c.ThumbSize() thumb.Size = c.ThumbSize()
thumb.Limit = c.ThumbLimit() thumb.Limit = c.ThumbLimit()

View File

@@ -20,13 +20,18 @@ func SetDbProvider(provider DbProvider) {
dbProvider = provider dbProvider = provider
} }
// Db() returns a database connection. // HasDbProvider returns true if a db provider exists.
func HasDbProvider() bool {
return dbProvider != nil
}
// Db returns a database connection.
func Db() *gorm.DB { func Db() *gorm.DB {
return dbProvider.Db() return dbProvider.Db()
} }
// Db() returns an unscoped database connection. // UnscopedDb returns an unscoped database connection.
func Unscoped() *gorm.DB { func UnscopedDb() *gorm.DB {
return Db().Unscoped() return Db().Unscoped()
} }

View File

@@ -10,6 +10,7 @@ https://github.com/photoprism/photoprism/wiki/Storage
package entity package entity
import ( import (
"sync"
"time" "time"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
@@ -17,6 +18,7 @@ import (
) )
var log = event.Log var log = event.Log
var resetFixturesOnce sync.Once
func logError(result *gorm.DB) { func logError(result *gorm.DB) {
if result.Error != nil { if result.Error != nil {
@@ -84,27 +86,38 @@ func ResetDb(testFixtures bool) {
DropTables() DropTables()
// Make sure changes have been written to disk. // Make sure changes have been written to disk.
time.Sleep(100 * time.Millisecond) time.Sleep(200 * time.Millisecond)
MigrateDb() MigrateDb()
if testFixtures { if testFixtures {
// Make sure changes have been written to disk. // Make sure changes have been written to disk.
time.Sleep(100 * time.Millisecond) time.Sleep(200 * time.Millisecond)
CreateTestFixtures() CreateTestFixtures()
} }
} }
// InitTestFixtures resets the database and test fixtures once.
func InitTestFixtures() {
resetFixturesOnce.Do(func() {
ResetDb(true)
})
}
// InitTestDb connects to and completely initializes the test database incl fixtures. // InitTestDb connects to and completely initializes the test database incl fixtures.
func InitTestDb(dsn string) *Gorm { func InitTestDb(dsn string) *Gorm {
if HasDbProvider() {
return nil
}
db := &Gorm{ db := &Gorm{
Driver: "mysql", Driver: "mysql",
Dsn: dsn, Dsn: dsn,
} }
SetDbProvider(db) SetDbProvider(db)
ResetDb(true) InitTestFixtures()
return db return db
} }

View File

@@ -19,7 +19,9 @@ func TestMain(m *testing.M) {
code := m.Run() code := m.Run()
if db != nil {
db.Close() db.Close()
}
os.Exit(code) os.Exit(code)
} }

View File

@@ -18,6 +18,7 @@ type Place struct {
LocKeywords string `gorm:"type:varchar(255);"` LocKeywords string `gorm:"type:varchar(255);"`
LocNotes string `gorm:"type:text;"` LocNotes string `gorm:"type:text;"`
LocFavorite bool LocFavorite bool
PhotoCount int
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
New bool `gorm:"-"` New bool `gorm:"-"`
@@ -33,6 +34,7 @@ var UnknownPlace = Place{
LocKeywords: "", LocKeywords: "",
LocNotes: "", LocNotes: "",
LocFavorite: false, LocFavorite: false,
PhotoCount: -1,
} }
// CreateUnknownPlace initializes default place in the database // CreateUnknownPlace initializes default place in the database

View File

@@ -1,9 +1,10 @@
package entity package entity
import ( import (
"github.com/stretchr/testify/assert"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
func TestCreateUnknownPlace(t *testing.T) { func TestCreateUnknownPlace(t *testing.T) {
@@ -32,7 +33,20 @@ func TestPlace_Find(t *testing.T) {
assert.Nil(t, r) assert.Nil(t, r)
}) })
t.Run("record does not exist", func(t *testing.T) { t.Run("record does not exist", func(t *testing.T) {
place := &Place{"1110", "test", "testCity", "", "", "", "", false, time.Now(), time.Now(), false} place := &Place{
ID: "1110",
LocLabel: "test",
LocCity: "testCity",
LocState: "",
LocCountry: "",
LocKeywords: "",
LocNotes: "",
LocFavorite: false,
PhotoCount: 0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
New: false,
}
r := place.Find() r := place.Find()
assert.Equal(t, "record not found", r.Error()) assert.Equal(t, "record not found", r.Error())
}) })

View File

@@ -1,6 +1,8 @@
package event package event
import "fmt" import (
"fmt"
)
func PublishEntities(name, ev string, entities interface{}) { func PublishEntities(name, ev string, entities interface{}) {
SharedHub().Publish(Message{ SharedHub().Publish(Message{

View File

@@ -14,6 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
@@ -42,7 +43,7 @@ func (imp *Import) originalsPath() string {
} }
// Start imports media files from a directory and converts/indexes them as needed. // Start imports media files from a directory and converts/indexes them as needed.
func (imp *Import) Start(opt ImportOptions) { func (imp *Import) Start(opt ImportOptions) map[string]bool {
var directories []string var directories []string
done := make(map[string]bool) done := make(map[string]bool)
ind := imp.index ind := imp.index
@@ -50,19 +51,19 @@ func (imp *Import) Start(opt ImportOptions) {
if !fs.PathExists(importPath) { if !fs.PathExists(importPath) {
event.Error(fmt.Sprintf("import: %s does not exist", importPath)) event.Error(fmt.Sprintf("import: %s does not exist", importPath))
return return done
} }
if err := mutex.Worker.Start(); err != nil { if err := mutex.Worker.Start(); err != nil {
event.Error(fmt.Sprintf("import: %s", err.Error())) event.Error(fmt.Sprintf("import: %s", err.Error()))
return return done
} }
defer mutex.Worker.Stop() defer mutex.Worker.Stop()
if err := ind.tensorFlow.Init(); err != nil { if err := ind.tensorFlow.Init(); err != nil {
log.Errorf("import: %s", err.Error()) log.Errorf("import: %s", err.Error())
return return done
} }
jobs := make(chan ImportJob) jobs := make(chan ImportJob)
@@ -194,7 +195,13 @@ func (imp *Import) Start(opt ImportOptions) {
log.Error(err.Error()) log.Error(err.Error())
} }
if err := query.UpdatePhotoCounts(); err != nil {
log.Errorf("import: %s", err)
}
runtime.GC() runtime.GC()
return done
} }
// Cancel stops the current import operation. // Cancel stops the current import operation.

View File

@@ -171,6 +171,10 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
log.Error(err.Error()) log.Error(err.Error())
} }
if err := query.UpdatePhotoCounts(); err != nil {
log.Errorf("index: %s", err)
}
runtime.GC() runtime.GC()
return done return done

View File

@@ -14,6 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/meta" "github.com/photoprism/photoprism/internal/meta"
"github.com/photoprism/photoprism/internal/nsfw" "github.com/photoprism/photoprism/internal/nsfw"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
@@ -426,7 +427,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
downloadedAs = originalName downloadedAs = originalName
} }
if err := ind.q.SetDownloadFileID(downloadedAs, file.ID); err != nil { if err := query.SetDownloadFileID(downloadedAs, file.ID); err != nil {
log.Errorf("index: %s", err) log.Errorf("index: %s", err)
} }

View File

@@ -4,12 +4,21 @@ import (
"os" "os"
"testing" "testing"
"github.com/photoprism/photoprism/internal/entity"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
log = logrus.StandardLogger() log = logrus.StandardLogger()
log.SetLevel(logrus.DebugLevel) log.SetLevel(logrus.DebugLevel)
db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DSN"))
code := m.Run() code := m.Run()
if db != nil {
db.Close()
}
os.Exit(code) os.Exit(code)
} }

View File

@@ -64,13 +64,11 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
runtime.GC() runtime.GC()
}() }()
q := query.New(prg.conf.Db())
limit := 500 limit := 500
offset := 0 offset := 0
for { for {
files, err := q.ExistingFiles(limit, offset, opt.Path) files, err := query.ExistingFiles(limit, offset, opt.Path)
if err != nil { if err != nil {
return purgedFiles, purgedPhotos, err return purgedFiles, purgedPhotos, err
@@ -118,7 +116,7 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
offset = 0 offset = 0
for { for {
photos, err := q.MissingPhotos(limit, offset) photos, err := query.MissingPhotos(limit, offset)
if err != nil { if err != nil {
return purgedFiles, purgedPhotos, err return purgedFiles, purgedPhotos, err
@@ -163,9 +161,15 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
offset += limit offset += limit
} }
err = q.ResetPhotosQuality() if err := query.ResetPhotosQuality(); err != nil {
return purgedFiles, purgedPhotos, err return purgedFiles, purgedPhotos, err
}
if err := query.UpdatePhotoCounts(); err != nil {
return purgedFiles, purgedPhotos, err
}
return purgedFiles, purgedPhotos, nil
} }
// Cancel stops the current purge operation. // Cancel stops the current purge operation.

View File

@@ -6,10 +6,8 @@ import (
) )
// AccountUploads a list of files for uploading to a remote account. // AccountUploads a list of files for uploading to a remote account.
func (q *Query) AccountUploads(a entity.Account, limit int) (results []entity.File, err error) { func AccountUploads(a entity.Account, limit int) (results []entity.File, err error) {
s := q.db s := Db().Where("files.file_missing = 0").
s = s.Where("files.file_missing = 0").
Where("files.id NOT IN (SELECT file_id FROM files_sync WHERE file_id > 0 AND account_id = ?)", a.ID) Where("files.id NOT IN (SELECT file_id FROM files_sync WHERE file_id > 0 AND account_id = ?)", a.ID)
if !a.SyncRaw { if !a.SyncRaw {

View File

@@ -5,19 +5,13 @@ import (
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/config"
) )
func TestQuery_AccountUploads(t *testing.T) { func TestAccountUploads(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
a := entity.Account{ID: 1, SyncRaw: false} a := entity.Account{ID: 1, SyncRaw: false}
t.Run("find uploads", func(t *testing.T) { t.Run("find uploads", func(t *testing.T) {
results, err := q.AccountUploads(a, 10) results, err := AccountUploads(a, 10)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@@ -6,8 +6,8 @@ import (
) )
// Accounts returns a list of accounts. // Accounts returns a list of accounts.
func (q *Query) Accounts(f form.AccountSearch) (result []entity.Account, err error) { func Accounts(f form.AccountSearch) (result []entity.Account, err error) {
s := q.db.Where(&entity.Account{}) s := Db().Where(&entity.Account{})
if f.Share { if f.Share {
s = s.Where("acc_share = 1") s = s.Where("acc_share = 1")
@@ -37,8 +37,8 @@ func (q *Query) Accounts(f form.AccountSearch) (result []entity.Account, err err
} }
// AccountByID finds an account by primary key. // AccountByID finds an account by primary key.
func (q *Query) AccountByID(id uint) (result entity.Account, err error) { func AccountByID(id uint) (result entity.Account, err error) {
if err := q.db.Where("id = ?", id).First(&result).Error; err != nil { if err := Db().Where("id = ?", id).First(&result).Error; err != nil {
return result, err return result, err
} }

View File

@@ -29,8 +29,8 @@ type AlbumResult struct {
} }
// AlbumByUUID returns a Album based on the UUID. // AlbumByUUID returns a Album based on the UUID.
func (q *Query) AlbumByUUID(albumUUID string) (album entity.Album, err error) { func AlbumByUUID(albumUUID string) (album entity.Album, err error) {
if err := q.db.Where("album_uuid = ?", albumUUID).Preload("Links").First(&album).Error; err != nil { if err := Db().Where("album_uuid = ?", albumUUID).Preload("Links").First(&album).Error; err != nil {
return album, err return album, err
} }
@@ -38,8 +38,8 @@ func (q *Query) AlbumByUUID(albumUUID string) (album entity.Album, err error) {
} }
// AlbumThumbByUUID returns a album preview file based on the uuid. // AlbumThumbByUUID returns a album preview file based on the uuid.
func (q *Query) AlbumThumbByUUID(albumUUID string) (file entity.File, err error) { func AlbumThumbByUUID(albumUUID string) (file entity.File, err error) {
if err := q.db.Where("files.file_primary = 1 AND files.deleted_at IS NULL"). if err := Db().Where("files.file_primary = 1 AND files.deleted_at IS NULL").
Joins("JOIN albums ON albums.album_uuid = ?", albumUUID). 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"). Joins("JOIN photos_albums pa ON pa.album_uuid = albums.album_uuid AND pa.photo_uuid = files.photo_uuid").
Joins("JOIN photos ON photos.id = files.photo_id AND photos.photo_private = 0 AND photos.deleted_at IS NULL"). Joins("JOIN photos ON photos.id = files.photo_id AND photos.photo_private = 0 AND photos.deleted_at IS NULL").
@@ -52,14 +52,14 @@ func (q *Query) AlbumThumbByUUID(albumUUID string) (file entity.File, err error)
} }
// Albums searches albums based on their name. // Albums searches albums based on their name.
func (q *Query) Albums(f form.AlbumSearch) (results []AlbumResult, err error) { func Albums(f form.AlbumSearch) (results []AlbumResult, err error) {
if err := f.ParseQueryString(); err != nil { if err := f.ParseQueryString(); err != nil {
return results, err return results, err
} }
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("albums: %+v", f))) defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("albums: %+v", f)))
s := q.db.NewScope(nil).DB() s := Db().NewScope(nil).DB()
s = s.Table("albums"). s = s.Table("albums").
Select(`albums.*, Select(`albums.*,

View File

@@ -1,20 +1,15 @@
package query package query
import ( import (
form "github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/photoprism/photoprism/internal/config" form "github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
) )
func TestQuery_AlbumByUUID(t *testing.T) { func TestAlbumByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("existing uuid", func(t *testing.T) { t.Run("existing uuid", func(t *testing.T) {
album, err := search.AlbumByUUID("at9lxuqxpogaaba7") album, err := AlbumByUUID("at9lxuqxpogaaba7")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -24,19 +19,15 @@ func TestQuery_AlbumByUUID(t *testing.T) {
}) })
t.Run("not existing uuid", func(t *testing.T) { t.Run("not existing uuid", func(t *testing.T) {
album, err := search.AlbumByUUID("3765") album, err := AlbumByUUID("3765")
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
t.Log(album) t.Log(album)
}) })
} }
func TestQuery_AlbumThumbByUUID(t *testing.T) { func TestAlbumThumbByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("existing uuid", func(t *testing.T) { t.Run("existing uuid", func(t *testing.T) {
file, err := search.AlbumThumbByUUID("at9lxuqxpogaaba8") file, err := AlbumThumbByUUID("at9lxuqxpogaaba8")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -46,20 +37,16 @@ func TestQuery_AlbumThumbByUUID(t *testing.T) {
}) })
t.Run("not existing uuid", func(t *testing.T) { t.Run("not existing uuid", func(t *testing.T) {
file, err := search.AlbumThumbByUUID("3765") file, err := AlbumThumbByUUID("3765")
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
t.Log(file) t.Log(file)
}) })
} }
func TestQuery_Albums(t *testing.T) { func TestAlbums(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("search with string", func(t *testing.T) { t.Run("search with string", func(t *testing.T) {
query := form.NewAlbumSearch("chr") query := form.NewAlbumSearch("chr")
result, err := search.Albums(query) result, err := Albums(query)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -70,7 +57,7 @@ func TestQuery_Albums(t *testing.T) {
t.Run("search with slug", func(t *testing.T) { t.Run("search with slug", func(t *testing.T) {
query := form.NewAlbumSearch("slug:holiday count:10") query := form.NewAlbumSearch("slug:holiday count:10")
result, err := search.Albums(query) result, err := Albums(query)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -82,7 +69,7 @@ func TestQuery_Albums(t *testing.T) {
t.Run("favorites true", func(t *testing.T) { t.Run("favorites true", func(t *testing.T) {
query := form.NewAlbumSearch("favorites:true count:10000") query := form.NewAlbumSearch("favorites:true count:10000")
result, err := search.Albums(query) result, err := Albums(query)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -93,7 +80,7 @@ func TestQuery_Albums(t *testing.T) {
t.Run("empty query", func(t *testing.T) { t.Run("empty query", func(t *testing.T) {
query := form.NewAlbumSearch("order:slug") query := form.NewAlbumSearch("order:slug")
result, err := search.Albums(query) result, err := Albums(query)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -103,7 +90,7 @@ func TestQuery_Albums(t *testing.T) {
}) })
t.Run("search with invalid query string", func(t *testing.T) { t.Run("search with invalid query string", func(t *testing.T) {
query := form.NewAlbumSearch("xxx:bla") query := form.NewAlbumSearch("xxx:bla")
result, err := search.Albums(query) result, err := Albums(query)
assert.Error(t, err, "unknown filter") assert.Error(t, err, "unknown filter")
t.Log(result) t.Log(result)
}) })

View File

@@ -9,8 +9,8 @@ type CategoryLabel struct {
Title string Title string
} }
func (q *Query) CategoryLabels(limit, offset int) (results []CategoryLabel) { func CategoryLabels(limit, offset int) (results []CategoryLabel) {
s := q.db.NewScope(nil).DB() s := Db().NewScope(nil).DB()
s = s.Table("categories"). s = s.Table("categories").
Select("label_name AS name"). Select("label_name AS name").

View File

@@ -0,0 +1,13 @@
package query
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCategoryLabels(t *testing.T) {
categories := CategoryLabels(1000, 0)
assert.GreaterOrEqual(t, 1, len(categories))
}

View File

@@ -1,18 +0,0 @@
package query
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/photoprism/photoprism/internal/config"
)
func TestQuery_CategoryLabels(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
categories := search.CategoryLabels(1000, 0)
assert.GreaterOrEqual(t, 1, len(categories))
}

View File

@@ -1,41 +0,0 @@
package query
import (
"testing"
"github.com/photoprism/photoprism/internal/entity"
"github.com/stretchr/testify/assert"
)
func TestNewCountry(t *testing.T) {
t.Run("name Fantasy code fy", func(t *testing.T) {
country := entity.NewCountry("fy", "Fantasy")
assert.Equal(t, "fy", country.ID)
assert.Equal(t, "Fantasy", country.CountryName)
assert.Equal(t, "fantasy", country.CountrySlug)
})
t.Run("name Unknown code Unknown", func(t *testing.T) {
country := entity.NewCountry("", "")
assert.Equal(t, "zz", country.ID)
assert.Equal(t, "Unknown", country.CountryName)
assert.Equal(t, "zz", country.CountrySlug)
})
}
func TestCountry_FirstOrCreate(t *testing.T) {
t.Run("country already existing", func(t *testing.T) {
country := entity.NewCountry("de", "Germany")
country.FirstOrCreate()
assert.Equal(t, "de", country.Code())
assert.Equal(t, "Germany", country.Name())
assert.Equal(t, "Country description", country.CountryDescription)
assert.Equal(t, "Country Notes", country.CountryNotes)
assert.Equal(t, uint(0), country.CountryPhotoID)
})
t.Run("country not yet existing", func(t *testing.T) {
country := entity.NewCountry("wl", "Wonder Land")
country.FirstOrCreate()
assert.Equal(t, "wl", country.Code())
assert.Equal(t, "Wonder Land", country.Name())
})
}

18
internal/query/counts.go Normal file
View File

@@ -0,0 +1,18 @@
package query
import "github.com/jinzhu/gorm"
// UpdatePhotoCounts updates photos count in related tables as needed.
func UpdatePhotoCounts() error {
/*
UPDATE places
SET
photo_count = (SELECT
COUNT(*) FROM
photos ph
WHERE places.id = ph.place_id AND ph.photo_quality >= 0 AND ph.deleted_at IS NULL)
*/
return Db().Table("places").
UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(*) FROM photos ph WHERE places.id = ph.place_id AND ph.photo_quality >= 0 AND ph.deleted_at IS NULL)")).Error
}

View File

@@ -0,0 +1,13 @@
package query
import (
"testing"
)
func TestUpdatePhotoCounts(t *testing.T) {
err := UpdatePhotoCounts()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -7,8 +7,8 @@ import (
) )
// FileShares returns up to 100 file shares for a given account id and status. // FileShares returns up to 100 file shares for a given account id and status.
func (q *Query) FileShares(accountId uint, status string) (result []entity.FileShare, err error) { func FileShares(accountId uint, status string) (result []entity.FileShare, err error) {
s := q.db.Where(&entity.FileShare{}) s := Db().Where(&entity.FileShare{})
if accountId > 0 { if accountId > 0 {
s = s.Where("account_id = ?", accountId) s = s.Where("account_id = ?", accountId)
@@ -31,12 +31,12 @@ func (q *Query) FileShares(accountId uint, status string) (result []entity.FileS
} }
// ExpiredFileShares returns up to 100 expired file shares for a given account. // ExpiredFileShares returns up to 100 expired file shares for a given account.
func (q *Query) ExpiredFileShares(account entity.Account) (result []entity.FileShare, err error) { func ExpiredFileShares(account entity.Account) (result []entity.FileShare, err error) {
if account.ShareExpires <= 0 { if account.ShareExpires <= 0 {
return result, nil return result, nil
} }
s := q.db.Where(&entity.FileShare{}) s := Db().Where(&entity.FileShare{})
exp := time.Now().Add(time.Duration(-1*account.ShareExpires) * time.Second) exp := time.Now().Add(time.Duration(-1*account.ShareExpires) * time.Second)

View File

@@ -8,7 +8,7 @@ import (
) )
// SetDownloadFileID updates the local file id for remote downloads. // SetDownloadFileID updates the local file id for remote downloads.
func (q *Query) SetDownloadFileID(filename string, fileId uint) error { func SetDownloadFileID(filename string, fileId uint) error {
if len(filename) == 0 { if len(filename) == 0 {
return errors.New("sync: can't update, filename empty") return errors.New("sync: can't update, filename empty")
} }
@@ -18,7 +18,7 @@ func (q *Query) SetDownloadFileID(filename string, fileId uint) error {
filename = string(os.PathSeparator) + filename filename = string(os.PathSeparator) + filename
} }
result := q.db.Model(entity.FileSync{}). result := Db().Model(entity.FileSync{}).
Where("remote_name = ? AND status = ? AND file_id = 0", filename, entity.FileSyncDownloaded). Where("remote_name = ? AND status = ? AND file_id = 0", filename, entity.FileSyncDownloaded).
Update("file_id", fileId) Update("file_id", fileId)

View File

@@ -5,8 +5,8 @@ import (
) )
// FileSyncs returns a list of FileSync entities for a given account and status. // FileSyncs returns a list of FileSync entities for a given account and status.
func (q *Query) FileSyncs(accountId uint, status string, limit int) (result []entity.FileSync, err error) { func FileSyncs(accountId uint, status string, limit int) (result []entity.FileSync, err error) {
s := q.db.Where(&entity.FileSync{}) s := Db().Where(&entity.FileSync{})
if accountId > 0 { if accountId > 0 {
s = s.Where("account_id = ?", accountId) s = s.Where("account_id = ?", accountId)

View File

@@ -7,12 +7,12 @@ import (
) )
// ExistingFiles returns not-missing and not-deleted file entities in the range of limit and offset sorted by id. // ExistingFiles returns not-missing and not-deleted file entities in the range of limit and offset sorted by id.
func (q *Query) ExistingFiles(limit int, offset int, filePath string) (files []entity.File, err error) { func ExistingFiles(limit int, offset int, filePath string) (files []entity.File, err error) {
if strings.HasPrefix(filePath, "/") { if strings.HasPrefix(filePath, "/") {
filePath = filePath[1:] filePath = filePath[1:]
} }
stmt := q.db.Unscoped().Where("file_missing = 0 AND deleted_at IS NULL") stmt := Db().Unscoped().Where("file_missing = 0 AND deleted_at IS NULL")
if filePath != "" { if filePath != "" {
stmt = stmt.Where("file_name LIKE ?", filePath+"/%") stmt = stmt.Where("file_name LIKE ?", filePath+"/%")
@@ -24,8 +24,8 @@ func (q *Query) ExistingFiles(limit int, offset int, filePath string) (files []e
} }
// FilesByUUID // FilesByUUID
func (q *Query) FilesByUUID(u []string, limit int, offset int) (files []entity.File, err error) { func FilesByUUID(u []string, limit int, offset int) (files []entity.File, err error) {
if err := q.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 { if err := 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, err
} }
@@ -33,8 +33,8 @@ func (q *Query) FilesByUUID(u []string, limit int, offset int) (files []entity.F
} }
// FileByPhotoUUID // FileByPhotoUUID
func (q *Query) FileByPhotoUUID(u string) (file entity.File, err error) { func FileByPhotoUUID(u string) (file entity.File, err error) {
if err := q.db.Where("photo_uuid = ? AND file_primary = 1", u).Preload("Links").Preload("Photo").First(&file).Error; err != nil { if err := Db().Where("photo_uuid = ? AND file_primary = 1", u).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
return file, err return file, err
} }
@@ -42,8 +42,8 @@ func (q *Query) FileByPhotoUUID(u string) (file entity.File, err error) {
} }
// FileByUUID returns the file entity for a given UUID. // FileByUUID returns the file entity for a given UUID.
func (q *Query) FileByUUID(uuid string) (file entity.File, err error) { func FileByUUID(uuid string) (file entity.File, err error) {
if err := q.db.Where("file_uuid = ?", uuid).Preload("Links").Preload("Photo").First(&file).Error; err != nil { if err := Db().Where("file_uuid = ?", uuid).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
return file, err return file, err
} }
@@ -51,8 +51,8 @@ func (q *Query) FileByUUID(uuid string) (file entity.File, err error) {
} }
// FirstFileByHash finds a file with a given hash string. // FirstFileByHash finds a file with a given hash string.
func (q *Query) FileByHash(fileHash string) (file entity.File, err error) { func FileByHash(fileHash string) (file entity.File, err error) {
if err := q.db.Where("file_hash = ?", fileHash).Preload("Links").Preload("Photo").First(&file).Error; err != nil { if err := Db().Where("file_hash = ?", fileHash).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
return file, err return file, err
} }
@@ -60,7 +60,7 @@ func (q *Query) FileByHash(fileHash string) (file entity.File, err error) {
} }
// SetPhotoPrimary sets a new primary image file for a photo. // SetPhotoPrimary sets a new primary image file for a photo.
func (q *Query) SetPhotoPrimary(photoUUID, fileUUID string) error { func SetPhotoPrimary(photoUUID, fileUUID string) error {
q.db.Model(entity.File{}).Where("photo_uuid = ? AND file_uuid <> ?", photoUUID, fileUUID).UpdateColumn("file_primary", false) Db().Model(entity.File{}).Where("photo_uuid = ? AND file_uuid <> ?", photoUUID, fileUUID).UpdateColumn("file_primary", false)
return q.db.Model(entity.File{}).Where("photo_uuid = ? AND file_uuid = ?", photoUUID, fileUUID).UpdateColumn("file_primary", true).Error return Db().Model(entity.File{}).Where("photo_uuid = ? AND file_uuid = ?", photoUUID, fileUUID).UpdateColumn("file_primary", true).Error
} }

View File

@@ -1,32 +1,25 @@
package query package query
import ( import (
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/photoprism/photoprism/internal/config" "github.com/stretchr/testify/assert"
) )
func TestQuery_ExistingFiles(t *testing.T) { func TestExistingFiles(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("files found", func(t *testing.T) { t.Run("files found", func(t *testing.T) {
files, err := search.ExistingFiles(1000, 0, "/") files, err := ExistingFiles(1000, 0, "/")
t.Logf("files: %+v", files)
assert.Nil(t, err) assert.Nil(t, err)
assert.LessOrEqual(t, 5, len(files)) assert.LessOrEqual(t, 5, len(files))
}) })
} }
func TestQuery_FilesByUUID(t *testing.T) { func TestFilesByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("files found", func(t *testing.T) { t.Run("files found", func(t *testing.T) {
files, err := search.FilesByUUID([]string{"ft8es39w45bnlqdw"}, 100, 0) files, err := FilesByUUID([]string{"ft8es39w45bnlqdw"}, 100, 0)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(files)) assert.Equal(t, 1, len(files))
@@ -34,33 +27,25 @@ func TestQuery_FilesByUUID(t *testing.T) {
}) })
} }
func TestQuery_FileByPhotoUUID(t *testing.T) { func TestFileByPhotoUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("files found", func(t *testing.T) { t.Run("files found", func(t *testing.T) {
file, err := search.FileByPhotoUUID("pt9jtdre2lvl0yh8") file, err := FileByPhotoUUID("pt9jtdre2lvl0yh8")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "exampleDNGFile.dng", file.FileName) assert.Equal(t, "exampleDNGFile.dng", file.FileName)
}) })
t.Run("no files found", func(t *testing.T) { t.Run("no files found", func(t *testing.T) {
file, err := search.FileByPhotoUUID("111") file, err := FileByPhotoUUID("111")
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
t.Log(file) t.Log(file)
}) })
} }
func TestQuery_FileByUUID(t *testing.T) { func TestFileByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("files found", func(t *testing.T) { t.Run("files found", func(t *testing.T) {
file, err := search.FileByUUID("ft8es39w45bnlqdw") file, err := FileByUUID("ft8es39w45bnlqdw")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -70,7 +55,7 @@ func TestQuery_FileByUUID(t *testing.T) {
}) })
t.Run("no files found", func(t *testing.T) { t.Run("no files found", func(t *testing.T) {
file, err := search.FileByUUID("111") file, err := FileByUUID("111")
if err == nil { if err == nil {
t.Fatal("error expected") t.Fatal("error expected")
@@ -81,20 +66,16 @@ func TestQuery_FileByUUID(t *testing.T) {
}) })
} }
func TestQuery_FileByHash(t *testing.T) { func TestFileByHash(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("files found", func(t *testing.T) { t.Run("files found", func(t *testing.T) {
file, err := search.FileByHash("2cad9168fa6acc5c5c2965ddf6ec465ca42fd818") file, err := FileByHash("2cad9168fa6acc5c5c2965ddf6ec465ca42fd818")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "exampleFileName.jpg", file.FileName) assert.Equal(t, "exampleFileName.jpg", file.FileName)
}) })
t.Run("no files found", func(t *testing.T) { t.Run("no files found", func(t *testing.T) {
file, err := search.FileByHash("111") file, err := FileByHash("111")
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
t.Log(file) t.Log(file)

View File

@@ -12,37 +12,15 @@ import (
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
// GeoResult represents a photo for displaying it on a map.
type GeoResult struct {
ID string `json:"ID"`
PhotoLat float32 `json:"Lat"`
PhotoLng float32 `json:"Lng"`
PhotoUUID string `json:"PhotoUUID"`
PhotoTitle string `json:"PhotoTitle"`
PhotoFavorite bool `json:"PhotoFavorite"`
FileHash string `json:"FileHash"`
FileWidth int `json:"FileWidth"`
FileHeight int `json:"FileHeight"`
TakenAt time.Time `json:"TakenAt"`
}
func (g GeoResult) Lat() float64 {
return float64(g.PhotoLat)
}
func (g GeoResult) Lng() float64 {
return float64(g.PhotoLng)
}
// Geo searches for photos based on a Form and returns a PhotoResult slice. // Geo searches for photos based on a Form and returns a PhotoResult slice.
func (q *Query) Geo(f form.GeoSearch) (results []GeoResult, err error) { func Geo(f form.GeoSearch) (results []GeoResult, err error) {
if err := f.ParseQueryString(); err != nil { if err := f.ParseQueryString(); err != nil {
return results, err return results, err
} }
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("search: %+v", f))) defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("search: %+v", f)))
s := q.db.NewScope(nil).DB() s := UnscopedDb()
s = s.Table("photos"). s = s.Table("photos").
Select(`photos.id, photos.photo_uuid, photos.photo_lat, photos.photo_lng, photos.photo_title, Select(`photos.id, photos.photo_uuid, photos.photo_lat, photos.photo_lng, photos.photo_title,

View File

@@ -0,0 +1,27 @@
package query
import (
"time"
)
// GeoResult represents a photo for displaying it on a map.
type GeoResult struct {
ID string `json:"ID"`
PhotoLat float32 `json:"Lat"`
PhotoLng float32 `json:"Lng"`
PhotoUUID string `json:"PhotoUUID"`
PhotoTitle string `json:"PhotoTitle"`
PhotoFavorite bool `json:"PhotoFavorite"`
FileHash string `json:"FileHash"`
FileWidth int `json:"FileWidth"`
FileHeight int `json:"FileHeight"`
TakenAt time.Time `json:"TakenAt"`
}
func (g GeoResult) Lat() float64 {
return float64(g.PhotoLat)
}
func (g GeoResult) Lng() float64 {
return float64(g.PhotoLng)
}

View File

@@ -1,21 +1,16 @@
package query package query
import ( import (
"github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
) )
func TestQuery_Geo(t *testing.T) { func TestGeo(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("search all photos", func(t *testing.T) { t.Run("search all photos", func(t *testing.T) {
query := form.NewGeoSearch("") query := form.NewGeoSearch("")
result, err := search.Geo(query) result, err := Geo(query)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 4, len(result)) assert.Equal(t, 4, len(result))
@@ -24,7 +19,7 @@ func TestQuery_Geo(t *testing.T) {
t.Run("search for bridge", func(t *testing.T) { t.Run("search for bridge", func(t *testing.T) {
query := form.NewGeoSearch("Query:bridge Before:3006-01-02") query := form.NewGeoSearch("Query:bridge Before:3006-01-02")
result, err := search.Geo(query) result, err := Geo(query)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "Neckarbrücke", result[0].PhotoTitle) assert.Equal(t, "Neckarbrücke", result[0].PhotoTitle)
@@ -33,7 +28,7 @@ func TestQuery_Geo(t *testing.T) {
t.Run("search for timeframe", func(t *testing.T) { t.Run("search for timeframe", func(t *testing.T) {
query := form.NewGeoSearch("After:2014-12-02 Before:3006-01-02") query := form.NewGeoSearch("After:2014-12-02 Before:3006-01-02")
result, err := search.Geo(query) result, err := Geo(query)
assert.Nil(t, err) assert.Nil(t, err)
t.Log(result) t.Log(result)

View File

@@ -0,0 +1,23 @@
package query
import (
"time"
)
// LabelResult contains found labels
type LabelResult struct {
// Label
ID uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
LabelUUID string
LabelSlug string
CustomSlug string
LabelName string
LabelPriority int
LabelCount int
LabelFavorite bool
LabelDescription string
LabelNotes string
}

View File

@@ -12,27 +12,9 @@ import (
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
// LabelResult contains found labels
type LabelResult struct {
// Label
ID uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
LabelUUID string
LabelSlug string
CustomSlug string
LabelName string
LabelPriority int
LabelCount int
LabelFavorite bool
LabelDescription string
LabelNotes string
}
// PhotoLabel returns a photo label entity if exists. // PhotoLabel returns a photo label entity if exists.
func (q *Query) PhotoLabel(photoID, labelID uint) (label entity.PhotoLabel, err error) { func PhotoLabel(photoID, labelID uint) (label entity.PhotoLabel, err error) {
if err := q.db.Where("photo_id = ? AND label_id = ?", photoID, labelID).Preload("Photo").Preload("Label").First(&label).Error; err != nil { if err := Db().Where("photo_id = ? AND label_id = ?", photoID, labelID).Preload("Photo").Preload("Label").First(&label).Error; err != nil {
return label, err return label, err
} }
@@ -40,8 +22,8 @@ func (q *Query) PhotoLabel(photoID, labelID uint) (label entity.PhotoLabel, err
} }
// LabelBySlug returns a Label based on the slug name. // LabelBySlug returns a Label based on the slug name.
func (q *Query) LabelBySlug(labelSlug string) (label entity.Label, err error) { func LabelBySlug(labelSlug string) (label entity.Label, err error) {
if err := q.db.Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).Preload("Links").First(&label).Error; err != nil { if err := Db().Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).Preload("Links").First(&label).Error; err != nil {
return label, err return label, err
} }
@@ -49,8 +31,8 @@ func (q *Query) LabelBySlug(labelSlug string) (label entity.Label, err error) {
} }
// LabelByUUID returns a Label based on the label UUID. // LabelByUUID returns a Label based on the label UUID.
func (q *Query) LabelByUUID(labelUUID string) (label entity.Label, err error) { func LabelByUUID(labelUUID string) (label entity.Label, err error) {
if err := q.db.Where("label_uuid = ?", labelUUID).Preload("Links").First(&label).Error; err != nil { if err := Db().Where("label_uuid = ?", labelUUID).Preload("Links").First(&label).Error; err != nil {
return label, err return label, err
} }
@@ -58,8 +40,8 @@ func (q *Query) LabelByUUID(labelUUID string) (label entity.Label, err error) {
} }
// LabelThumbBySlug returns a label preview file based on the slug name. // LabelThumbBySlug returns a label preview file based on the slug name.
func (q *Query) LabelThumbBySlug(labelSlug string) (file entity.File, err error) { func LabelThumbBySlug(labelSlug string) (file entity.File, err error) {
if err := q.db.Where("files.file_primary AND files.deleted_at IS NULL"). if err := Db().Where("files.file_primary AND files.deleted_at IS NULL").
Joins("JOIN labels ON labels.label_slug = ?", labelSlug). 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"). Joins("JOIN photos_labels ON photos_labels.label_id = labels.id AND photos_labels.photo_id = files.photo_id").
Joins("JOIN photos ON photos.id = files.photo_id AND photos.photo_private = 0 AND photos.deleted_at IS NULL"). Joins("JOIN photos ON photos.id = files.photo_id AND photos.photo_private = 0 AND photos.deleted_at IS NULL").
@@ -72,9 +54,9 @@ func (q *Query) LabelThumbBySlug(labelSlug string) (file entity.File, err error)
} }
// LabelThumbByUUID returns a label preview file based on the label UUID. // LabelThumbByUUID returns a label preview file based on the label UUID.
func (q *Query) LabelThumbByUUID(labelUUID string) (file entity.File, err error) { func LabelThumbByUUID(labelUUID string) (file entity.File, err error) {
// Search matching label // Search matching label
err = q.db.Where("files.file_primary AND files.deleted_at IS NULL"). err = Db().Where("files.file_primary AND files.deleted_at IS NULL").
Joins("JOIN labels ON labels.label_uuid = ?", labelUUID). Joins("JOIN labels ON labels.label_uuid = ?", labelUUID).
Joins("JOIN photos_labels ON photos_labels.label_id = labels.id AND photos_labels.photo_id = files.photo_id"). Joins("JOIN photos_labels ON photos_labels.label_id = labels.id AND photos_labels.photo_id = files.photo_id").
Joins("JOIN photos ON photos.id = files.photo_id AND photos.photo_private = 0 AND photos.deleted_at IS NULL"). Joins("JOIN photos ON photos.id = files.photo_id AND photos.photo_private = 0 AND photos.deleted_at IS NULL").
@@ -86,7 +68,7 @@ func (q *Query) LabelThumbByUUID(labelUUID string) (file entity.File, err error)
} }
// If failed, search for category instead // If failed, search for category instead
err = q.db.Where("files.file_primary AND files.deleted_at IS NULL"). err = Db().Where("files.file_primary AND files.deleted_at IS NULL").
Joins("JOIN photos_labels ON photos_labels.photo_id = files.photo_id"). Joins("JOIN photos_labels ON photos_labels.photo_id = files.photo_id").
Joins("JOIN categories c ON photos_labels.label_id = c.label_id"). Joins("JOIN categories c ON photos_labels.label_id = c.label_id").
Joins("JOIN labels ON c.category_id = labels.id AND labels.label_uuid= ?", labelUUID). Joins("JOIN labels ON c.category_id = labels.id AND labels.label_uuid= ?", labelUUID).
@@ -98,14 +80,14 @@ func (q *Query) LabelThumbByUUID(labelUUID string) (file entity.File, err error)
} }
// Labels searches labels based on their name. // Labels searches labels based on their name.
func (q *Query) Labels(f form.LabelSearch) (results []LabelResult, err error) { func Labels(f form.LabelSearch) (results []LabelResult, err error) {
if err := f.ParseQueryString(); err != nil { if err := f.ParseQueryString(); err != nil {
return results, err return results, err
} }
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("labels: %+v", f))) defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("labels: %+v", f)))
s := q.db.NewScope(nil).DB() s := UnscopedDb()
// s.LogMode(true) // s.LogMode(true)
@@ -132,14 +114,14 @@ func (q *Query) Labels(f form.LabelSearch) (results []LabelResult, err error) {
slugString := slug.Make(f.Query) slugString := slug.Make(f.Query)
likeString := "%" + strings.ToLower(f.Query) + "%" likeString := "%" + strings.ToLower(f.Query) + "%"
if result := q.db.First(&label, "label_slug = ? OR custom_slug = ?", slugString, slugString); result.Error != nil { if result := Db().First(&label, "label_slug = ? OR custom_slug = ?", slugString, slugString); result.Error != nil {
log.Infof("search: label %s not found", txt.Quote(f.Query)) log.Infof("search: label %s not found", txt.Quote(f.Query))
s = s.Where("LOWER(labels.label_name) LIKE ?", likeString) s = s.Where("LOWER(labels.label_name) LIKE ?", likeString)
} else { } else {
labelIds = append(labelIds, label.ID) labelIds = append(labelIds, label.ID)
q.db.Where("category_id = ?", label.ID).Find(&categories) Db().Where("category_id = ?", label.ID).Find(&categories)
for _, category := range categories { for _, category := range categories {
labelIds = append(labelIds, category.LabelID) labelIds = append(labelIds, category.LabelID)

View File

@@ -1,21 +1,16 @@
package query package query
import ( import (
"testing"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing"
"github.com/photoprism/photoprism/internal/config"
) )
func TestQuery_LabelBySlug(t *testing.T) { func TestLabelBySlug(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
t.Run("files found", func(t *testing.T) { t.Run("files found", func(t *testing.T) {
label, err := q.LabelBySlug("flower") label, err := LabelBySlug("flower")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -25,20 +20,16 @@ func TestQuery_LabelBySlug(t *testing.T) {
}) })
t.Run("no files found", func(t *testing.T) { t.Run("no files found", func(t *testing.T) {
label, err := q.LabelBySlug("111") label, err := LabelBySlug("111")
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
assert.Empty(t, label.ID) assert.Empty(t, label.ID)
}) })
} }
func TestQuery_LabelByUUID(t *testing.T) { func TestLabelByUUID(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
t.Run("files found", func(t *testing.T) { t.Run("files found", func(t *testing.T) {
label, err := q.LabelByUUID("lt9k3pw1wowuy3c5") label, err := LabelByUUID("lt9k3pw1wowuy3c5")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -48,20 +39,16 @@ func TestQuery_LabelByUUID(t *testing.T) {
}) })
t.Run("no files found", func(t *testing.T) { t.Run("no files found", func(t *testing.T) {
label, err := q.LabelByUUID("111") label, err := LabelByUUID("111")
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
assert.Empty(t, label.ID) assert.Empty(t, label.ID)
}) })
} }
func TestQuery_LabelThumbBySlug(t *testing.T) { func TestLabelThumbBySlug(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
t.Run("files found", func(t *testing.T) { t.Run("files found", func(t *testing.T) {
file, err := q.LabelThumbBySlug("flower") file, err := LabelThumbBySlug("flower")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -71,20 +58,16 @@ func TestQuery_LabelThumbBySlug(t *testing.T) {
}) })
t.Run("no files found", func(t *testing.T) { t.Run("no files found", func(t *testing.T) {
file, err := q.LabelThumbBySlug("cow") file, err := LabelThumbBySlug("cow")
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
t.Log(file) t.Log(file)
}) })
} }
func TestQuery_LabelThumbByUUID(t *testing.T) { func TestLabelThumbByUUID(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
t.Run("files found", func(t *testing.T) { t.Run("files found", func(t *testing.T) {
file, err := q.LabelThumbByUUID("lt9k3pw1wowuy3c4") file, err := LabelThumbByUUID("lt9k3pw1wowuy3c4")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -94,20 +77,17 @@ func TestQuery_LabelThumbByUUID(t *testing.T) {
}) })
t.Run("no files found", func(t *testing.T) { t.Run("no files found", func(t *testing.T) {
file, err := q.LabelThumbByUUID("14") file, err := LabelThumbByUUID("14")
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
t.Log(file) t.Log(file)
}) })
} }
func TestQuery_Labels(t *testing.T) { func TestLabels(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
t.Run("search with query", func(t *testing.T) { t.Run("search with query", func(t *testing.T) {
query := form.NewLabelSearch("Query:C Count:1005 Order:slug") query := form.NewLabelSearch("Query:C Count:1005 Order:slug")
result, err := q.Labels(query) result, err := Labels(query)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -134,7 +114,7 @@ func TestQuery_Labels(t *testing.T) {
t.Run("search for favorites", func(t *testing.T) { t.Run("search for favorites", func(t *testing.T) {
query := form.NewLabelSearch("Favorites:true") query := form.NewLabelSearch("Favorites:true")
result, err := q.Labels(query) result, err := Labels(query)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -160,7 +140,7 @@ func TestQuery_Labels(t *testing.T) {
t.Run("search with empty query", func(t *testing.T) { t.Run("search with empty query", func(t *testing.T) {
query := form.NewLabelSearch("") query := form.NewLabelSearch("")
result, err := q.Labels(query) result, err := Labels(query)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -171,7 +151,7 @@ func TestQuery_Labels(t *testing.T) {
t.Run("search with invalid query string", func(t *testing.T) { t.Run("search with invalid query string", func(t *testing.T) {
query := form.NewLabelSearch("xxx:bla") query := form.NewLabelSearch("xxx:bla")
result, err := q.Labels(query) result, err := Labels(query)
assert.Error(t, err, "unknown filter") assert.Error(t, err, "unknown filter")
assert.Empty(t, result) assert.Empty(t, result)

View File

@@ -8,8 +8,8 @@ type MomentsTimeResult struct {
} }
// GetMomentsTime counts photos per month and year // GetMomentsTime counts photos per month and year
func (q *Query) GetMomentsTime() (results []MomentsTimeResult, err error) { func GetMomentsTime() (results []MomentsTimeResult, err error) {
s := q.db.NewScope(nil).DB() s := UnscopedDb()
s = s.Table("photos"). s = s.Table("photos").
Where("deleted_at IS NULL"). Where("deleted_at IS NULL").

View File

@@ -1,19 +1,14 @@
package query package query
import ( import (
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/photoprism/photoprism/internal/config" "github.com/stretchr/testify/assert"
) )
func TestQuery_GetMomentsTime(t *testing.T) { func TestGetMomentsTime(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("result found", func(t *testing.T) { t.Run("result found", func(t *testing.T) {
result, err := search.GetMomentsTime() result, err := GetMomentsTime()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 2790, result[0].PhotoYear) assert.Equal(t, 2790, result[0].PhotoYear)

View File

@@ -1,407 +1,13 @@
package query package query
import ( import (
"fmt"
"strings"
"time"
"github.com/gosimple/slug"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/capture"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/ulule/deepcopier"
) )
// 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
TakenSrc string
TimeZone string
PhotoUUID string
PhotoPath string
PhotoName string
PhotoTitle string
PhotoYear int
PhotoMonth int
PhotoCountry string
PhotoFavorite bool
PhotoPrivate bool
PhotoLat float32
PhotoLng float32
PhotoAltitude int
PhotoIso int
PhotoFocalLength int
PhotoFNumber float32
PhotoExposure string
PhotoQuality int
PhotoResolution int
Merged bool
// Camera
CameraID uint
CameraModel string
CameraMake string
// Lens
LensID uint
LensModel string
LensMake string
// Location
LocationID string
PlaceID string
LocLabel string
LocCity string
LocState string
LocCountry string
// File
FileID uint
FileUUID string
FilePrimary bool
FileMissing bool
FileName string
FileHash string
FileType string
FileMime string
FileWidth int
FileHeight int
FileOrientation int
FileAspectRatio float32
FileColors string // todo: remove from result?
FileChroma uint8 // todo: remove from result?
FileLuminance string // todo: remove from result?
FileDiff uint32 // todo: remove from result?
Files []entity.File
}
type PhotoResults []PhotoResult
func (m PhotoResults) Merged() (PhotoResults, int, error) {
count := len(m)
merged := make([]PhotoResult, 0, count)
var lastId uint
var i int
for _, res := range m {
file := entity.File{}
if err := deepcopier.Copy(&file).From(res); err != nil {
return merged, count, err
}
file.ID = res.FileID
if lastId == res.ID && i > 0 {
merged[i-1].Files = append(merged[i-1].Files, file)
merged[i-1].Merged = true
continue
}
lastId = res.ID
res.Files = append(res.Files, file)
merged = append(merged, res)
i++
}
return merged, count, nil
}
func (m *PhotoResult) ShareFileName() string {
var name string
if m.PhotoTitle != "" {
name = strings.Title(slug.MakeLang(m.PhotoTitle, "en"))
} else {
name = m.PhotoUUID
}
taken := m.TakenAtLocal.Format("20060102-150405")
token := rnd.Token(3)
result := fmt.Sprintf("%s-%s-%s.%s", taken, name, token, m.FileType)
return result
}
// Photos searches for photos based on a Form and returns a PhotoResult slice.
func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err error) {
if err := f.ParseQueryString(); err != nil {
return results, 0, err
}
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("photos: %+v", f)))
s := q.db.NewScope(nil).DB()
// s.LogMode(true)
s = s.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,
files.file_diff,
cameras.camera_make, cameras.camera_model,
lenses.lens_make, lenses.lens_model,
places.loc_label, places.loc_city, places.loc_state, places.loc_country
`).
Joins("JOIN files ON files.photo_id = photos.id AND files.file_type = 'jpg' AND files.file_missing = 0 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("JOIN places ON photos.place_id = places.id").
Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id AND photos_labels.uncertainty < 100").
Group("photos.id, files.id")
if f.ID != "" {
s = s.Where("photos.photo_uuid = ?", f.ID)
s = s.Order("files.file_primary DESC")
if result := s.Scan(&results); result.Error != nil {
return results, 0, result.Error
}
if f.Merged {
return results.Merged()
}
return results, len(results), nil
}
var categories []entity.Category
var label entity.Label
var labelIds []uint
if f.Label != "" {
slugString := strings.ToLower(f.Label)
if result := q.db.First(&label, "label_slug =? OR custom_slug = ?", slugString, slugString); result.Error != nil {
log.Errorf("search: label %s not found", txt.Quote(f.Label))
return results, 0, fmt.Errorf("label %s not found", txt.Quote(f.Label))
} else {
labelIds = append(labelIds, label.ID)
q.db.Where("category_id = ?", label.ID).Find(&categories)
for _, category := range categories {
labelIds = append(labelIds, category.LabelID)
}
s = s.Where("photos_labels.label_id IN (?)", labelIds)
}
}
if f.Location == true {
s = s.Where("location_id > 0")
if f.Query != "" {
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id").
Where("keywords.keyword LIKE ?", strings.ToLower(txt.Clip(f.Query, txt.ClipKeyword))+"%")
}
} else if f.Query != "" {
if len(f.Query) < 2 {
return results, 0, fmt.Errorf("query too short")
}
slugString := slug.Make(f.Query)
lowerString := strings.ToLower(f.Query)
likeString := txt.Clip(lowerString, txt.ClipKeyword) + "%"
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id")
if result := q.db.First(&label, "label_slug = ? OR custom_slug = ?", slugString, slugString); result.Error != nil {
log.Infof("search: label %s not found, using fuzzy search", txt.Quote(f.Query))
s = s.Where("keywords.keyword LIKE ?", likeString)
} else {
labelIds = append(labelIds, label.ID)
q.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", txt.Quote(label.LabelName), len(labelIds))
s = s.Where("photos_labels.label_id IN (?) OR keywords.keyword LIKE ?", labelIds, likeString)
}
}
if f.Archived {
s = s.Where("photos.deleted_at IS NOT NULL")
} else {
s = s.Where("photos.deleted_at IS NULL")
if f.Private {
s = s.Where("photos.photo_private = 1")
} else if f.Public {
s = s.Where("photos.photo_private = 0")
}
if f.Review {
s = s.Where("photos.photo_quality < 3")
} else if f.Quality != 0 && f.Private == false {
s = s.Where("photos.photo_quality >= ?", f.Quality)
}
}
if f.Error {
s = s.Where("files.file_error <> ''")
}
if f.Album != "" {
s = s.Joins("JOIN photos_albums ON photos_albums.photo_uuid = photos.photo_uuid").Where("photos_albums.album_uuid = ?", f.Album)
}
if f.Camera > 0 {
s = s.Where("photos.camera_id = ?", f.Camera)
}
if f.Lens > 0 {
s = s.Where("photos.lens_id = ?", f.Lens)
}
if f.Year > 0 {
s = s.Where("photos.photo_year = ?", f.Year)
}
if f.Month > 0 {
s = s.Where("photos.photo_month = ?", f.Month)
}
if f.Color != "" {
s = s.Where("files.file_main_color = ?", strings.ToLower(f.Color))
}
if f.Favorites {
s = s.Where("photos.photo_favorite = 1")
}
if f.Story {
s = s.Where("photos.photo_story = 1")
}
if f.Country != "" {
s = s.Where("photos.photo_country = ?", f.Country)
}
if f.Title != "" {
s = s.Where("LOWER(photos.photo_title) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Title)))
}
if f.Hash != "" {
s = s.Where("files.file_hash = ?", f.Hash)
}
if f.Duplicate {
s = s.Where("files.file_duplicate = 1")
}
if f.Portrait {
s = s.Where("files.file_portrait = 1")
}
if f.Mono {
s = s.Where("files.file_chroma = 0")
} else if f.Chroma > 9 {
s = s.Where("files.file_chroma > ?", f.Chroma)
} else if f.Chroma > 0 {
s = s.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma)
}
if f.Diff != 0 {
s = s.Where("files.file_diff = ?", f.Diff)
}
if f.Fmin > 0 {
s = s.Where("photos.photo_f_number >= ?", f.Fmin)
}
if f.Fmax > 0 {
s = s.Where("photos.photo_f_number <= ?", f.Fmax)
}
if f.Dist == 0 {
f.Dist = 20
} else if f.Dist > 5000 {
f.Dist = 5000
}
// Inaccurate distance search, but probably 'good enough' for now
if f.Lat > 0 {
latMin := f.Lat - SearchRadius*float32(f.Dist)
latMax := f.Lat + SearchRadius*float32(f.Dist)
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
}
if f.Lng > 0 {
lngMin := f.Lng - SearchRadius*float32(f.Dist)
lngMax := f.Lng + SearchRadius*float32(f.Dist)
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
}
if !f.Before.IsZero() {
s = s.Where("photos.taken_at <= ?", f.Before.Format("2006-01-02"))
}
if !f.After.IsZero() {
s = s.Where("photos.taken_at >= ?", f.After.Format("2006-01-02"))
}
switch f.Order {
case entity.SortOrderRelevance:
if f.Label != "" {
s = s.Order("photo_quality DESC, photos_labels.uncertainty ASC, taken_at DESC, files.file_primary DESC")
} else {
s = s.Order("photo_quality DESC, taken_at DESC, files.file_primary DESC")
}
case entity.SortOrderNewest:
s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC")
case entity.SortOrderOldest:
s = s.Order("taken_at, photos.photo_uuid, files.file_primary DESC")
case entity.SortOrderImported:
s = s.Order("photos.id DESC, files.file_primary DESC")
case entity.SortOrderSimilar:
s = s.Order("files.file_main_color, photos.location_id, files.file_diff, taken_at DESC, files.file_primary DESC")
default:
s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC")
}
if f.Count > 0 && f.Count <= 1000 {
s = s.Limit(f.Count).Offset(f.Offset)
} else {
s = s.Limit(100).Offset(0)
}
if result := s.Scan(&results); result.Error != nil {
return results, 0, result.Error
}
if f.Merged {
return results.Merged()
}
return results, len(results), nil
}
// PhotoByID returns a Photo based on the ID. // PhotoByID returns a Photo based on the ID.
func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) { func PhotoByID(photoID uint64) (photo entity.Photo, err error) {
if err := q.db.Unscoped().Where("id = ?", photoID). if err := UnscopedDb().Where("id = ?", photoID).
Preload("Links"). Preload("Links").
Preload("Description"). Preload("Description").
Preload("Location"). Preload("Location").
@@ -418,8 +24,8 @@ func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) {
} }
// PhotoByUUID returns a Photo based on the UUID. // PhotoByUUID returns a Photo based on the UUID.
func (q *Query) PhotoByUUID(photoUUID string) (photo entity.Photo, err error) { func PhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
if err := q.db.Unscoped().Where("photo_uuid = ?", photoUUID). if err := UnscopedDb().Where("photo_uuid = ?", photoUUID).
Preload("Links"). Preload("Links").
Preload("Description"). Preload("Description").
Preload("Location"). Preload("Location").
@@ -436,8 +42,8 @@ func (q *Query) PhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
} }
// PreloadPhotoByUUID returns a Photo based on the UUID with all dependencies preloaded. // PreloadPhotoByUUID returns a Photo based on the UUID with all dependencies preloaded.
func (q *Query) PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err error) { func PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
if err := q.db.Unscoped().Where("photo_uuid = ?", photoUUID). if err := UnscopedDb().Where("photo_uuid = ?", photoUUID).
Preload("Labels", func(db *gorm.DB) *gorm.DB { Preload("Labels", func(db *gorm.DB) *gorm.DB {
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC") return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
}). }).
@@ -458,8 +64,8 @@ func (q *Query) PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err er
} }
// MissingPhotos returns photo entities without existing files. // MissingPhotos returns photo entities without existing files.
func (q *Query) MissingPhotos(limit int, offset int) (entities []entity.Photo, err error) { func MissingPhotos(limit int, offset int) (entities []entity.Photo, err error) {
err = q.db. err = Db().
Select("photos.*"). Select("photos.*").
Joins("JOIN files a ON photos.id = a.photo_id "). Joins("JOIN files a ON photos.id = a.photo_id ").
Joins("LEFT JOIN files b ON a.photo_id = b.photo_id AND a.id != b.id AND b.file_missing = 0"). Joins("LEFT JOIN files b ON a.photo_id = b.photo_id AND a.id != b.id AND b.file_missing = 0").
@@ -471,8 +77,8 @@ func (q *Query) MissingPhotos(limit int, offset int) (entities []entity.Photo, e
} }
// ResetPhotosQuality resets the quality of photos without primary file to -1. // ResetPhotosQuality resets the quality of photos without primary file to -1.
func (q *Query) ResetPhotosQuality() error { func ResetPhotosQuality() error {
return q.db.Table("photos"). return Db().Table("photos").
Where("id IN (SELECT photos.id FROM photos LEFT JOIN files ON photos.id = files.photo_id AND files.file_primary = 1 WHERE files.id IS NULL GROUP BY photos.id)"). Where("id IN (SELECT photos.id FROM photos LEFT JOIN files ON photos.id = files.photo_id AND files.file_primary = 1 WHERE files.id IS NULL GROUP BY photos.id)").
Update("photo_quality", -1).Error Update("photo_quality", -1).Error
} }

View File

@@ -0,0 +1,134 @@
package query
import (
"fmt"
"strings"
"time"
"github.com/gosimple/slug"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/ulule/deepcopier"
)
// 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
TakenSrc string
TimeZone string
PhotoUUID string
PhotoPath string
PhotoName string
PhotoTitle string
PhotoYear int
PhotoMonth int
PhotoCountry string
PhotoFavorite bool
PhotoPrivate bool
PhotoLat float32
PhotoLng float32
PhotoAltitude int
PhotoIso int
PhotoFocalLength int
PhotoFNumber float32
PhotoExposure string
PhotoQuality int
PhotoResolution int
Merged bool
// Camera
CameraID uint
CameraModel string
CameraMake string
// Lens
LensID uint
LensModel string
LensMake string
// Location
LocationID string
PlaceID string
LocLabel string
LocCity string
LocState string
LocCountry string
// File
FileID uint
FileUUID string
FilePrimary bool
FileMissing bool
FileName string
FileHash string
FileType string
FileMime string
FileWidth int
FileHeight int
FileOrientation int
FileAspectRatio float32
FileColors string // todo: remove from result?
FileChroma uint8 // todo: remove from result?
FileLuminance string // todo: remove from result?
FileDiff uint32 // todo: remove from result?
Files []entity.File
}
type PhotoResults []PhotoResult
func (m PhotoResults) Merged() (PhotoResults, int, error) {
count := len(m)
merged := make([]PhotoResult, 0, count)
var lastId uint
var i int
for _, res := range m {
file := entity.File{}
if err := deepcopier.Copy(&file).From(res); err != nil {
return merged, count, err
}
file.ID = res.FileID
if lastId == res.ID && i > 0 {
merged[i-1].Files = append(merged[i-1].Files, file)
merged[i-1].Merged = true
continue
}
lastId = res.ID
res.Files = append(res.Files, file)
merged = append(merged, res)
i++
}
return merged, count, nil
}
func (m *PhotoResult) ShareFileName() string {
var name string
if m.PhotoTitle != "" {
name = strings.Title(slug.MakeLang(m.PhotoTitle, "en"))
} else {
name = m.PhotoUUID
}
taken := m.TakenAtLocal.Format("20060102-150405")
token := rnd.Token(3)
result := fmt.Sprintf("%s-%s-%s.%s", taken, name, token, m.FileType)
return result
}

View File

@@ -4,9 +4,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/form"
) )
/*func TestQuery_Photos(t *testing.T) { /*func TestQuery_Photos(t *testing.T) {
@@ -28,324 +25,44 @@ import (
}) })
}*/ }*/
func TestQuery_PhotoByID(t *testing.T) { func TestPhotoByID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("photo found", func(t *testing.T) { t.Run("photo found", func(t *testing.T) {
result, err := search.PhotoByID(1000000) result, err := PhotoByID(1000000)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 2790, result.PhotoYear) assert.Equal(t, 2790, result.PhotoYear)
}) })
t.Run("no photo found", func(t *testing.T) { t.Run("no photo found", func(t *testing.T) {
result, err := search.PhotoByID(99999) result, err := PhotoByID(99999)
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
t.Log(result) t.Log(result)
}) })
} }
func TestQuery_PhotoByUUID(t *testing.T) { func TestPhotoByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("photo found", func(t *testing.T) { t.Run("photo found", func(t *testing.T) {
result, err := search.PhotoByUUID("pt9jtdre2lvl0y12") result, err := PhotoByUUID("pt9jtdre2lvl0y12")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "Reunion", result.PhotoTitle) assert.Equal(t, "Reunion", result.PhotoTitle)
}) })
t.Run("no photo found", func(t *testing.T) { t.Run("no photo found", func(t *testing.T) {
result, err := search.PhotoByUUID("99999") result, err := PhotoByUUID("99999")
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
t.Log(result) t.Log(result)
}) })
} }
func TestQuery_PreloadPhotoByUUID(t *testing.T) { func TestPreloadPhotoByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
t.Run("photo found", func(t *testing.T) { t.Run("photo found", func(t *testing.T) {
result, err := search.PreloadPhotoByUUID("pt9jtdre2lvl0y12") result, err := PreloadPhotoByUUID("pt9jtdre2lvl0y12")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "Reunion", result.PhotoTitle) assert.Equal(t, "Reunion", result.PhotoTitle)
}) })
t.Run("no photo found", func(t *testing.T) { t.Run("no photo found", func(t *testing.T) {
result, err := search.PreloadPhotoByUUID("99999") result, err := PreloadPhotoByUUID("99999")
assert.Error(t, err, "record not found") assert.Error(t, err, "record not found")
t.Log(result) t.Log(result)
}) })
} }
func TestSearch_Photos(t *testing.T) {
conf := config.TestConfig()
conf.CreateDirectories()
search := New(conf.Db())
t.Run("normal query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("label query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "label:dog"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
// TODO: Add database fixtures to avoid failing queries
t.Logf("query failed: %s", err.Error())
// t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("invalid label query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "label:xxx"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
assert.Error(t, err)
assert.Empty(t, photos)
if err != nil {
assert.Equal(t, err.Error(), "label xxx not found")
}
})
t.Run("form.location true", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Location = true
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.camera", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Camera = 2
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.color", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Color = "blue"
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.favorites", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "favorites:true"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.country", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "country:de"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.title", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "title:Pug Dog"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.hash", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "hash:xxx"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.duplicate", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "duplicate:true"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.portrait", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "portrait:true"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.mono", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "mono:true"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.chroma", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "chroma:50"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.fmin and Order:oldest", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Fmin:5 Order:oldest"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.fmax and Order:newest", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Fmax:2 Order:newest"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.Lat and form.Lng and Order:imported", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Lat:33.45343166666667 Lng:25.764711666666667 Dist:2000 Order:imported"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.Before and form.After", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Before:2005-01-01 After:2003-01-01"
f.Count = 5000
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
}

275
internal/query/photos.go Normal file
View File

@@ -0,0 +1,275 @@
package query
import (
"fmt"
"strings"
"time"
"github.com/gosimple/slug"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/capture"
"github.com/photoprism/photoprism/pkg/txt"
)
// Photos searches for photos based on a Form and returns a PhotoResult slice.
func Photos(f form.PhotoSearch) (results PhotoResults, count int, err error) {
if err := f.ParseQueryString(); err != nil {
return results, 0, err
}
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("photos: %+v", f)))
s := UnscopedDb()
// s.LogMode(true)
s = s.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,
files.file_diff,
cameras.camera_make, cameras.camera_model,
lenses.lens_make, lenses.lens_model,
places.loc_label, places.loc_city, places.loc_state, places.loc_country
`).
Joins("JOIN files ON files.photo_id = photos.id AND files.file_type = 'jpg' AND files.file_missing = 0 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("JOIN places ON photos.place_id = places.id").
Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id AND photos_labels.uncertainty < 100").
Group("photos.id, files.id")
if f.ID != "" {
s = s.Where("photos.photo_uuid = ?", f.ID)
s = s.Order("files.file_primary DESC")
if result := s.Scan(&results); result.Error != nil {
return results, 0, result.Error
}
if f.Merged {
return results.Merged()
}
return results, len(results), nil
}
var categories []entity.Category
var label entity.Label
var labelIds []uint
if f.Label != "" {
slugString := strings.ToLower(f.Label)
if result := Db().First(&label, "label_slug =? OR custom_slug = ?", slugString, slugString); result.Error != nil {
log.Errorf("search: label %s not found", txt.Quote(f.Label))
return results, 0, fmt.Errorf("label %s not found", txt.Quote(f.Label))
} else {
labelIds = append(labelIds, label.ID)
Db().Where("category_id = ?", label.ID).Find(&categories)
for _, category := range categories {
labelIds = append(labelIds, category.LabelID)
}
s = s.Where("photos_labels.label_id IN (?)", labelIds)
}
}
if f.Location == true {
s = s.Where("location_id > 0")
if f.Query != "" {
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id").
Where("keywords.keyword LIKE ?", strings.ToLower(txt.Clip(f.Query, txt.ClipKeyword))+"%")
}
} else if f.Query != "" {
if len(f.Query) < 2 {
return results, 0, fmt.Errorf("query too short")
}
slugString := slug.Make(f.Query)
lowerString := strings.ToLower(f.Query)
likeString := txt.Clip(lowerString, txt.ClipKeyword) + "%"
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id")
if result := Db().First(&label, "label_slug = ? OR custom_slug = ?", slugString, slugString); result.Error != nil {
log.Infof("search: label %s not found, using fuzzy search", txt.Quote(f.Query))
s = s.Where("keywords.keyword LIKE ?", likeString)
} else {
labelIds = append(labelIds, label.ID)
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", txt.Quote(label.LabelName), len(labelIds))
s = s.Where("photos_labels.label_id IN (?) OR keywords.keyword LIKE ?", labelIds, likeString)
}
}
if f.Archived {
s = s.Where("photos.deleted_at IS NOT NULL")
} else {
s = s.Where("photos.deleted_at IS NULL")
if f.Private {
s = s.Where("photos.photo_private = 1")
} else if f.Public {
s = s.Where("photos.photo_private = 0")
}
if f.Review {
s = s.Where("photos.photo_quality < 3")
} else if f.Quality != 0 && f.Private == false {
s = s.Where("photos.photo_quality >= ?", f.Quality)
}
}
if f.Error {
s = s.Where("files.file_error <> ''")
}
if f.Album != "" {
s = s.Joins("JOIN photos_albums ON photos_albums.photo_uuid = photos.photo_uuid").Where("photos_albums.album_uuid = ?", f.Album)
}
if f.Camera > 0 {
s = s.Where("photos.camera_id = ?", f.Camera)
}
if f.Lens > 0 {
s = s.Where("photos.lens_id = ?", f.Lens)
}
if f.Year > 0 {
s = s.Where("photos.photo_year = ?", f.Year)
}
if f.Month > 0 {
s = s.Where("photos.photo_month = ?", f.Month)
}
if f.Color != "" {
s = s.Where("files.file_main_color = ?", strings.ToLower(f.Color))
}
if f.Favorites {
s = s.Where("photos.photo_favorite = 1")
}
if f.Story {
s = s.Where("photos.photo_story = 1")
}
if f.Country != "" {
s = s.Where("photos.photo_country = ?", f.Country)
}
if f.Title != "" {
s = s.Where("LOWER(photos.photo_title) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Title)))
}
if f.Hash != "" {
s = s.Where("files.file_hash = ?", f.Hash)
}
if f.Duplicate {
s = s.Where("files.file_duplicate = 1")
}
if f.Portrait {
s = s.Where("files.file_portrait = 1")
}
if f.Mono {
s = s.Where("files.file_chroma = 0")
} else if f.Chroma > 9 {
s = s.Where("files.file_chroma > ?", f.Chroma)
} else if f.Chroma > 0 {
s = s.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma)
}
if f.Diff != 0 {
s = s.Where("files.file_diff = ?", f.Diff)
}
if f.Fmin > 0 {
s = s.Where("photos.photo_f_number >= ?", f.Fmin)
}
if f.Fmax > 0 {
s = s.Where("photos.photo_f_number <= ?", f.Fmax)
}
if f.Dist == 0 {
f.Dist = 20
} else if f.Dist > 5000 {
f.Dist = 5000
}
// Inaccurate distance search, but probably 'good enough' for now
if f.Lat > 0 {
latMin := f.Lat - SearchRadius*float32(f.Dist)
latMax := f.Lat + SearchRadius*float32(f.Dist)
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
}
if f.Lng > 0 {
lngMin := f.Lng - SearchRadius*float32(f.Dist)
lngMax := f.Lng + SearchRadius*float32(f.Dist)
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
}
if !f.Before.IsZero() {
s = s.Where("photos.taken_at <= ?", f.Before.Format("2006-01-02"))
}
if !f.After.IsZero() {
s = s.Where("photos.taken_at >= ?", f.After.Format("2006-01-02"))
}
switch f.Order {
case entity.SortOrderRelevance:
if f.Label != "" {
s = s.Order("photo_quality DESC, photos_labels.uncertainty ASC, taken_at DESC, files.file_primary DESC")
} else {
s = s.Order("photo_quality DESC, taken_at DESC, files.file_primary DESC")
}
case entity.SortOrderNewest:
s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC")
case entity.SortOrderOldest:
s = s.Order("taken_at, photos.photo_uuid, files.file_primary DESC")
case entity.SortOrderImported:
s = s.Order("photos.id DESC, files.file_primary DESC")
case entity.SortOrderSimilar:
s = s.Order("files.file_main_color, photos.location_id, files.file_diff, taken_at DESC, files.file_primary DESC")
default:
s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC")
}
if f.Count > 0 && f.Count <= 1000 {
s = s.Limit(f.Count).Offset(f.Offset)
} else {
s = s.Limit(100).Offset(0)
}
if result := s.Scan(&results); result.Error != nil {
return results, 0, result.Error
}
if f.Merged {
return results.Merged()
}
return results, len(results), nil
}

View File

@@ -0,0 +1,271 @@
package query
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/form"
)
func TestPhotos(t *testing.T) {
t.Run("normal query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("label query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "label:dog"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
// TODO: Add database fixtures to avoid failing queries
t.Logf("query failed: %s", err.Error())
// t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("invalid label query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "label:xxx"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
assert.Error(t, err)
assert.Empty(t, photos)
if err != nil {
assert.Equal(t, err.Error(), "label xxx not found")
}
})
t.Run("form.location true", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Location = true
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.camera", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Camera = 2
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.color", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Color = "blue"
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.favorites", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "favorites:true"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.country", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "country:de"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.title", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "title:Pug Dog"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.hash", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "hash:xxx"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.duplicate", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "duplicate:true"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.portrait", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "portrait:true"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.mono", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "mono:true"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.chroma", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "chroma:50"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.fmin and Order:oldest", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Fmin:5 Order:oldest"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.fmax and Order:newest", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Fmax:2 Order:newest"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.Lat and form.Lng and Order:imported", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Lat:33.45343166666667 Lng:25.764711666666667 Dist:2000 Order:imported"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.Before and form.After", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Before:2005-01-01 After:2003-01-01"
f.Count = 5000
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
}

View File

@@ -8,6 +8,7 @@ https://github.com/photoprism/photoprism/wiki
package query package query
import ( import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
@@ -36,3 +37,13 @@ func New(db *gorm.DB) *Query {
return q return q
} }
// Db returns a database connection instance.
func Db() *gorm.DB {
return entity.Db()
}
// UnscopedDb returns an unscoped database connection instance.
func UnscopedDb() *gorm.DB {
return entity.Db().Unscoped()
}

View File

@@ -4,6 +4,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/photoprism/photoprism/internal/entity"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -11,11 +12,13 @@ func TestMain(m *testing.M) {
log = logrus.StandardLogger() log = logrus.StandardLogger()
log.SetLevel(logrus.DebugLevel) log.SetLevel(logrus.DebugLevel)
// db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DSN")) db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DSN"))
code := m.Run() code := m.Run()
// db.Close() if db != nil {
db.Close()
}
os.Exit(code) os.Exit(code)
} }

View File

@@ -8,12 +8,12 @@ import (
) )
// PhotoSelection returns all selected photos. // PhotoSelection returns all selected photos.
func (q *Query) PhotoSelection(f form.Selection) (results []entity.Photo, err error) { func PhotoSelection(f form.Selection) (results []entity.Photo, err error) {
if f.Empty() { if f.Empty() {
return results, errors.New("no photos selected") return results, errors.New("no photos selected")
} }
s := q.db.NewScope(nil).DB() s := Db().NewScope(nil).DB()
s = s.Table("photos"). s = s.Table("photos").
Select("photos.*"). Select("photos.*").

View File

@@ -39,11 +39,8 @@ func (s *Share) Start() (err error) {
Share: true, Share: true,
} }
db := s.conf.Db()
q := query.New(db)
// Find accounts for which sharing is enabled // Find accounts for which sharing is enabled
accounts, err := q.Accounts(f) accounts, err := query.Accounts(f)
// Upload newly shared files // Upload newly shared files
for _, a := range accounts { for _, a := range accounts {
@@ -55,7 +52,7 @@ func (s *Share) Start() (err error) {
continue continue
} }
files, err := q.FileShares(a.ID, entity.FileShareNew) files, err := query.FileShares(a.ID, entity.FileShareNew)
if err != nil { if err != nil {
log.Errorf("share: %s", err.Error()) log.Errorf("share: %s", err.Error())
@@ -121,7 +118,7 @@ func (s *Share) Start() (err error) {
return nil return nil
} }
if err := db.Save(&file).Error; err != nil { if err := entity.Db().Save(&file).Error; err != nil {
log.Errorf("share: %s", err.Error()) log.Errorf("share: %s", err.Error())
} }
} }
@@ -137,7 +134,7 @@ func (s *Share) Start() (err error) {
continue continue
} }
files, err := q.ExpiredFileShares(a) files, err := query.ExpiredFileShares(a)
if err != nil { if err != nil {
log.Errorf("share: %s", err.Error()) log.Errorf("share: %s", err.Error())
@@ -166,7 +163,7 @@ func (s *Share) Start() (err error) {
file.Status = entity.FileShareRemoved file.Status = entity.FileShareRemoved
} }
if err := db.Save(&file).Error; err != nil { if err := entity.Db().Save(&file).Error; err != nil {
log.Errorf("share: %s", err.Error()) log.Errorf("share: %s", err.Error())
} }
} }

View File

@@ -16,14 +16,12 @@ import (
// Sync represents a sync worker. // Sync represents a sync worker.
type Sync struct { type Sync struct {
conf *config.Config conf *config.Config
q *query.Query
} }
// NewSync returns a new service sync worker. // NewSync returns a new service sync worker.
func NewSync(conf *config.Config) *Sync { func NewSync(conf *config.Config) *Sync {
return &Sync{ return &Sync{
conf: conf, conf: conf,
q: query.New(conf.Db()),
} }
} }
@@ -40,10 +38,7 @@ func (s *Sync) Start() (err error) {
Sync: true, Sync: true,
} }
db := s.conf.Db() accounts, err := query.Accounts(f)
q := s.q
accounts, err := q.Accounts(f)
for _, a := range accounts { for _, a := range accounts {
if a.AccType != remote.ServiceWebDAV { if a.AccType != remote.ServiceWebDAV {
@@ -117,7 +112,7 @@ func (s *Sync) Start() (err error) {
return nil return nil
} }
if err := db.First(&a, a.ID).Error; err != nil { if err := entity.Db().First(&a, a.ID).Error; err != nil {
log.Errorf("sync: %s", err.Error()) log.Errorf("sync: %s", err.Error())
return err return err
} }
@@ -128,7 +123,7 @@ func (s *Sync) Start() (err error) {
a.SyncStatus = syncStatus a.SyncStatus = syncStatus
a.SyncDate = syncDate a.SyncDate = syncDate
if err := db.Save(&a).Error; err != nil { if err := entity.Db().Save(&a).Error; err != nil {
log.Errorf("sync: %s", err.Error()) log.Errorf("sync: %s", err.Error())
} else if synced { } else if synced {
event.Publish("sync.synced", event.Data{"account": a}) event.Publish("sync.synced", event.Data{"account": a})

View File

@@ -8,6 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/remote/webdav" "github.com/photoprism/photoprism/internal/remote/webdav"
"github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
@@ -20,12 +21,13 @@ func (s *Sync) downloadPath() string {
return s.conf.TempPath() + "/sync" return s.conf.TempPath() + "/sync"
} }
// relatedDownloads returns files to be downloaded grouped by prefix.
func (s *Sync) relatedDownloads(a entity.Account) (result Downloads, err error) { func (s *Sync) relatedDownloads(a entity.Account) (result Downloads, err error) {
result = make(Downloads) result = make(Downloads)
maxResults := 1000 maxResults := 1000
// Get remote files from database // Get remote files from database
files, err := s.q.FileSyncs(a.ID, entity.FileSyncNew, maxResults) files, err := query.FileSyncs(a.ID, entity.FileSyncNew, maxResults)
if err != nil { if err != nil {
return result, err return result, err
@@ -48,10 +50,9 @@ func (s *Sync) relatedDownloads(a entity.Account) (result Downloads, err error)
// Downloads remote files in batches and imports / indexes them // Downloads remote files in batches and imports / indexes them
func (s *Sync) download(a entity.Account) (complete bool, err error) { func (s *Sync) download(a entity.Account) (complete bool, err error) {
db := s.conf.Db()
// Set up index worker // Set up index worker
indexJobs := make(chan photoprism.IndexJob) indexJobs := make(chan photoprism.IndexJob)
go photoprism.IndexWorker(indexJobs) go photoprism.IndexWorker(indexJobs)
defer close(indexJobs) defer close(indexJobs)
@@ -118,7 +119,7 @@ func (s *Sync) download(a entity.Account) (complete bool, err error) {
} }
} }
if err := db.Save(&file).Error; err != nil { if err := entity.Db().Save(&file).Error; err != nil {
log.Errorf("sync: %s", err.Error()) log.Errorf("sync: %s", err.Error())
} else { } else {
files[i] = file files[i] = file

View File

@@ -8,17 +8,16 @@ import (
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/remote/webdav" "github.com/photoprism/photoprism/internal/remote/webdav"
) )
// Uploads local files to a remote account // Uploads local files to a remote account
func (s *Sync) upload(a entity.Account) (complete bool, err error) { func (s *Sync) upload(a entity.Account) (complete bool, err error) {
db := s.conf.Db()
q := s.q
maxResults := 250 maxResults := 250
// Get upload file list from database // Get upload file list from database
files, err := q.AccountUploads(a, maxResults) files, err := query.AccountUploads(a, maxResults)
if err != nil { if err != nil {
return false, err return false, err
@@ -68,7 +67,7 @@ func (s *Sync) upload(a entity.Account) (complete bool, err error) {
return false, nil return false, nil
} }
if err := db.Save(&fileSync).Error; err != nil { if err := entity.Db().Save(&fileSync).Error; err != nil {
log.Errorf("sync: %s", err.Error()) log.Errorf("sync: %s", err.Error())
} }
} }