mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Backend: Query package refactoring
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"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/pkg/txt"
|
||||
)
|
||||
@@ -25,7 +25,6 @@ func GetAccounts(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
var f form.AccountSearch
|
||||
|
||||
q := service.Query()
|
||||
err := c.MustBindWith(&f, binding.Form)
|
||||
|
||||
if err != nil {
|
||||
@@ -33,7 +32,7 @@ func GetAccounts(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := q.Accounts(f)
|
||||
result, err := query.Accounts(f)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
@@ -59,10 +58,9 @@ func GetAccount(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
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)
|
||||
} else {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
|
||||
@@ -81,10 +79,9 @@ func GetAccountDirs(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
id := ParseUint(c.Param("id"))
|
||||
|
||||
m, err := q.AccountByID(id)
|
||||
m, err := query.AccountByID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
|
||||
@@ -114,10 +111,9 @@ func ShareWithAccount(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
id := ParseUint(c.Param("id"))
|
||||
|
||||
m, err := q.AccountByID(id)
|
||||
m, err := query.AccountByID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
|
||||
@@ -132,7 +128,7 @@ func ShareWithAccount(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
dst := f.Destination
|
||||
files, err := q.FilesByUUID(f.Photos, 1000, 0)
|
||||
files, err := query.FilesByUUID(f.Photos, 1000, 0)
|
||||
|
||||
if err != nil {
|
||||
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"))
|
||||
|
||||
q := service.Query()
|
||||
|
||||
m, err := q.AccountByID(id)
|
||||
m, err := query.AccountByID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -236,7 +230,7 @@ func UpdateAccount(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
event.Success("account saved")
|
||||
|
||||
m, err = q.AccountByID(id)
|
||||
m, err = query.AccountByID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
|
||||
@@ -259,9 +253,8 @@ func DeleteAccount(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
id := ParseUint(c.Param("id"))
|
||||
q := service.Query()
|
||||
|
||||
m, err := q.AccountByID(id)
|
||||
m, err := query.AccountByID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"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/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
@@ -35,7 +35,6 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
var f form.AlbumSearch
|
||||
|
||||
q := service.Query()
|
||||
err := c.MustBindWith(&f, binding.Form)
|
||||
|
||||
if err != nil {
|
||||
@@ -43,7 +42,7 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := q.Albums(f)
|
||||
result, err := query.Albums(f)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
@@ -61,8 +60,7 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
|
||||
func GetAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/albums/:uuid", func(c *gin.Context) {
|
||||
id := c.Param("uuid")
|
||||
q := service.Query()
|
||||
m, err := q.AlbumByUUID(id)
|
||||
m, err := query.AlbumByUUID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||
@@ -88,13 +86,12 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
m := entity.NewAlbum(f.AlbumName)
|
||||
m.AlbumFavorite = f.AlbumFavorite
|
||||
|
||||
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())
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("%s already exists", txt.Quote(m.AlbumName))})
|
||||
return
|
||||
@@ -104,7 +101,7 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
event.Publish("config.updated", event.Data(conf.ClientConfig()))
|
||||
|
||||
PublishAlbumEvent(EntityCreated, m.AlbumUUID, c, q)
|
||||
PublishAlbumEvent(EntityCreated, m.AlbumUUID, c)
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
})
|
||||
@@ -119,9 +116,7 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
uuid := c.Param("uuid")
|
||||
q := service.Query()
|
||||
|
||||
m, err := q.AlbumByUUID(uuid)
|
||||
m, err := query.AlbumByUUID(uuid)
|
||||
|
||||
if err != nil {
|
||||
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.Success("album saved")
|
||||
|
||||
PublishAlbumEvent(EntityUpdated, uuid, c, q)
|
||||
PublishAlbumEvent(EntityUpdated, uuid, c)
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
})
|
||||
@@ -166,16 +161,15 @@ func DeleteAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
id := c.Param("uuid")
|
||||
q := service.Query()
|
||||
|
||||
m, err := q.AlbumByUUID(id)
|
||||
m, err := query.AlbumByUUID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
PublishAlbumEvent(EntityDeleted, id, c, q)
|
||||
PublishAlbumEvent(EntityDeleted, id, c)
|
||||
|
||||
conf.Db().Delete(&m)
|
||||
|
||||
@@ -198,9 +192,7 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
id := c.Param("uuid")
|
||||
q := service.Query()
|
||||
|
||||
album, err := q.AlbumByUUID(id)
|
||||
album, err := query.AlbumByUUID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||
@@ -211,7 +203,7 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
conf.Db().Save(&album)
|
||||
|
||||
event.Publish("config.updated", event.Data(conf.ClientConfig()))
|
||||
PublishAlbumEvent(EntityUpdated, id, c, q)
|
||||
PublishAlbumEvent(EntityUpdated, id, c)
|
||||
|
||||
c.JSON(http.StatusOK, http.Response{})
|
||||
})
|
||||
@@ -229,8 +221,7 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
id := c.Param("uuid")
|
||||
q := service.Query()
|
||||
album, err := q.AlbumByUUID(id)
|
||||
album, err := query.AlbumByUUID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||
@@ -241,7 +232,7 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
conf.Db().Save(&album)
|
||||
|
||||
event.Publish("config.updated", event.Data(conf.ClientConfig()))
|
||||
PublishAlbumEvent(EntityUpdated, id, c, q)
|
||||
PublishAlbumEvent(EntityUpdated, id, c)
|
||||
|
||||
c.JSON(http.StatusOK, http.Response{})
|
||||
})
|
||||
@@ -263,15 +254,14 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
uuid := c.Param("uuid")
|
||||
q := service.Query()
|
||||
a, err := q.AlbumByUUID(uuid)
|
||||
a, err := query.AlbumByUUID(uuid)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
photos, err := q.PhotoSelection(f)
|
||||
photos, err := query.PhotoSelection(f)
|
||||
|
||||
if err != nil {
|
||||
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))
|
||||
}
|
||||
|
||||
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})
|
||||
})
|
||||
@@ -318,21 +308,18 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
a, err := q.AlbumByUUID(c.Param("uuid"))
|
||||
a, err := query.AlbumByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
db := conf.Db()
|
||||
|
||||
db.Where("album_uuid = ? AND photo_uuid IN (?)", a.AlbumUUID, f.Photos).Delete(&entity.PhotoAlbum{})
|
||||
entity.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))
|
||||
|
||||
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})
|
||||
})
|
||||
@@ -343,15 +330,14 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/albums/:uuid/download", func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
|
||||
q := service.Query()
|
||||
a, err := q.AlbumByUUID(c.Param("uuid"))
|
||||
a, err := query.AlbumByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
p, _, err := q.Photos(form.PhotoSearch{
|
||||
p, _, err := query.Photos(form.PhotoSearch{
|
||||
Album: a.AlbumUUID,
|
||||
Count: 10000,
|
||||
Offset: 0,
|
||||
@@ -384,7 +370,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
defer newZipFile.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(newZipFile)
|
||||
defer zipWriter.Close()
|
||||
defer func() { _ = zipWriter.Close() }()
|
||||
|
||||
for _, f := range p {
|
||||
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))
|
||||
zipWriter.Close()
|
||||
_ = zipWriter.Close()
|
||||
newZipFile.Close()
|
||||
|
||||
if !fs.FileExists(zipFileName) {
|
||||
@@ -443,8 +429,6 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
|
||||
gc := conf.Cache()
|
||||
cacheKey := fmt.Sprintf("album-thumbnail:%s:%s", uuid, typeName)
|
||||
|
||||
@@ -454,7 +438,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := q.AlbumThumbByUUID(uuid)
|
||||
f, err := query.AlbumThumbByUUID(uuid)
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
f.FileMissing = true
|
||||
conf.Db().Save(&f)
|
||||
entity.Db().Save(&f)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,11 @@ func BatchPhotosArchive(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
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())
|
||||
|
||||
@@ -80,9 +82,7 @@ func BatchPhotosRestore(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
log.Infof("restoring photos: %#v", f.Photos)
|
||||
|
||||
db := conf.Db()
|
||||
|
||||
db.Unscoped().Model(&entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).
|
||||
entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).
|
||||
UpdateColumn("deleted_at", gorm.Expr("NULL"))
|
||||
|
||||
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)
|
||||
|
||||
db := conf.Db()
|
||||
|
||||
db.Where("album_uuid IN (?)", f.Albums).Delete(&entity.Album{})
|
||||
db.Where("album_uuid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
|
||||
entity.Db().Where("album_uuid IN (?)", f.Albums).Delete(&entity.Album{})
|
||||
entity.Db().Where("album_uuid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
|
||||
|
||||
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)
|
||||
|
||||
db := conf.Db()
|
||||
|
||||
err := db.Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")).Error
|
||||
err := entity.Db().Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")).Error
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed)
|
||||
return
|
||||
}
|
||||
|
||||
q := query.New(db)
|
||||
|
||||
if entities, err := q.PhotoSelection(f); err == nil {
|
||||
if entities, err := query.PhotoSelection(f); err == nil {
|
||||
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)
|
||||
|
||||
db := conf.Db()
|
||||
|
||||
db.Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).Updates(map[string]interface{}{
|
||||
entity.Db().Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).Updates(map[string]interface{}{
|
||||
"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)
|
||||
|
||||
db := conf.Db()
|
||||
|
||||
db.Where("label_uuid IN (?)", f.Labels).Delete(&entity.Label{})
|
||||
entity.Db().Where("label_uuid IN (?)", f.Labels).Delete(&entity.Label{})
|
||||
|
||||
event.Publish("config.updated", event.Data(conf.ClientConfig()))
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"path"
|
||||
|
||||
"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/gin-gonic/gin"
|
||||
@@ -23,8 +24,7 @@ func GetDownload(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/download/:hash", func(c *gin.Context) {
|
||||
fileHash := c.Param("hash")
|
||||
|
||||
q := service.Query()
|
||||
f, err := q.FileByHash(fileHash)
|
||||
f, err := query.FileByHash(fileHash)
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
f.FileMissing = true
|
||||
conf.Db().Save(&f)
|
||||
entity.Db().Save(&f)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ const (
|
||||
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}
|
||||
result, _, err := q.Photos(f)
|
||||
result, _, err := query.Photos(f)
|
||||
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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}
|
||||
result, err := q.Albums(f)
|
||||
result, err := query.Albums(f)
|
||||
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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}
|
||||
result, err := q.Labels(f)
|
||||
result, err := query.Labels(f)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
@@ -22,8 +22,7 @@ func GetFile(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
p, err := q.FileByHash(c.Param("hash"))
|
||||
p, err := query.FileByHash(c.Param("hash"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -45,10 +44,7 @@ func LinkFile(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
db := conf.Db()
|
||||
q := query.New(db)
|
||||
|
||||
m, err := q.FileByUUID(c.Param("uuid"))
|
||||
m, err := query.FileByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
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())})
|
||||
return
|
||||
} else {
|
||||
db.Model(&m).Association("Links").Append(link)
|
||||
entity.Db().Model(&m).Association("Links").Append(link)
|
||||
}
|
||||
|
||||
event.Success("created file share link")
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"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/gin-gonic/gin"
|
||||
@@ -24,7 +24,6 @@ func GetGeo(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
var f form.GeoSearch
|
||||
|
||||
q := service.Query()
|
||||
err := c.MustBindWith(&f, binding.Form)
|
||||
|
||||
if err != nil {
|
||||
@@ -32,7 +31,7 @@ func GetGeo(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
photos, err := q.Geo(f)
|
||||
photos, err := query.Geo(f)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
|
||||
@@ -11,9 +11,10 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"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/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
@@ -29,7 +30,6 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
var f form.LabelSearch
|
||||
|
||||
q := service.Query()
|
||||
err := c.MustBindWith(&f, binding.Form)
|
||||
|
||||
if err != nil {
|
||||
@@ -37,7 +37,8 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := q.Labels(f)
|
||||
result, err := query.Labels(f)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
@@ -67,9 +68,7 @@ func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
id := c.Param("uuid")
|
||||
q := service.Query()
|
||||
|
||||
m, err := q.LabelByUUID(id)
|
||||
m, err := query.LabelByUUID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
|
||||
@@ -77,11 +76,11 @@ func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
m.SetName(f.LabelName)
|
||||
conf.Db().Save(&m)
|
||||
entity.Db().Save(&m)
|
||||
|
||||
event.Success("label saved")
|
||||
|
||||
PublishLabelEvent(EntityUpdated, id, c, q)
|
||||
PublishLabelEvent(EntityUpdated, id, c)
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
})
|
||||
@@ -99,9 +98,7 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
id := c.Param("uuid")
|
||||
q := service.Query()
|
||||
|
||||
label, err := q.LabelByUUID(id)
|
||||
label, err := query.LabelByUUID(id)
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
conf.Db().Save(&label)
|
||||
entity.Db().Save(&label)
|
||||
|
||||
if label.LabelPriority < 0 {
|
||||
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{})
|
||||
})
|
||||
@@ -135,9 +132,7 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
id := c.Param("uuid")
|
||||
q := service.Query()
|
||||
|
||||
label, err := q.LabelByUUID(id)
|
||||
label, err := query.LabelByUUID(id)
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
conf.Db().Save(&label)
|
||||
entity.Db().Save(&label)
|
||||
|
||||
if label.LabelPriority < 0 {
|
||||
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{})
|
||||
})
|
||||
@@ -180,8 +175,6 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
|
||||
gc := conf.Cache()
|
||||
cacheKey := fmt.Sprintf("label-thumbnail:%s:%s", labelUUID, typeName)
|
||||
|
||||
@@ -191,7 +184,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := q.LabelThumbByUUID(labelUUID)
|
||||
f, err := query.LabelThumbByUUID(labelUUID)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
|
||||
@@ -39,10 +39,7 @@ func LinkAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
db := conf.Db()
|
||||
q := query.New(db)
|
||||
|
||||
m, err := q.AlbumByUUID(c.Param("uuid"))
|
||||
m, err := query.AlbumByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
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())})
|
||||
return
|
||||
} else {
|
||||
db.Model(&m).Association("Links").Append(link)
|
||||
entity.Db().Model(&m).Association("Links").Append(link)
|
||||
}
|
||||
|
||||
event.Success("created album share link")
|
||||
@@ -70,10 +67,7 @@ func LinkPhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
db := conf.Db()
|
||||
q := query.New(db)
|
||||
|
||||
m, err := q.PhotoByUUID(c.Param("uuid"))
|
||||
m, err := query.PhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
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())})
|
||||
return
|
||||
} else {
|
||||
db.Model(&m).Association("Links").Append(link)
|
||||
entity.Db().Model(&m).Association("Links").Append(link)
|
||||
}
|
||||
|
||||
event.Success("created photo share link")
|
||||
@@ -101,10 +95,7 @@ func LinkLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
db := conf.Db()
|
||||
q := query.New(db)
|
||||
|
||||
m, err := q.LabelByUUID(c.Param("uuid"))
|
||||
m, err := query.LabelByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
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())})
|
||||
return
|
||||
} else {
|
||||
db.Model(&m).Association("Links").Append(link)
|
||||
entity.Db().Model(&m).Association("Links").Append(link)
|
||||
}
|
||||
|
||||
event.Success("created label share link")
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"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/gin-gonic/gin"
|
||||
@@ -18,9 +18,8 @@ func GetMomentsTime(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
result, err := query.GetMomentsTime()
|
||||
|
||||
result, err := q.GetMomentsTime()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
@@ -26,8 +25,7 @@ func GetPhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
p, err := q.PreloadPhotoByUUID(c.Param("uuid"))
|
||||
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -46,11 +44,8 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
db := conf.Db()
|
||||
uuid := c.Param("uuid")
|
||||
q := query.New(db)
|
||||
|
||||
m, err := q.PhotoByUUID(uuid)
|
||||
m, err := query.PhotoByUUID(uuid)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -81,11 +76,15 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
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")
|
||||
|
||||
p, err := q.PreloadPhotoByUUID(uuid)
|
||||
p, err := query.PreloadPhotoByUUID(uuid)
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/photos/:uuid/download", func(c *gin.Context) {
|
||||
q := service.Query()
|
||||
f, err := q.FileByPhotoUUID(c.Param("uuid"))
|
||||
f, err := query.FileByPhotoUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -142,8 +140,7 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
id := c.Param("uuid")
|
||||
q := service.Query()
|
||||
m, err := q.PhotoByUUID(id)
|
||||
m, err := query.PhotoByUUID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -158,7 +155,7 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
"count": 1,
|
||||
})
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, id, c, q)
|
||||
PublishPhotoEvent(EntityUpdated, id, c)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
@@ -176,8 +173,7 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
}
|
||||
|
||||
id := c.Param("uuid")
|
||||
q := service.Query()
|
||||
m, err := q.PhotoByUUID(id)
|
||||
m, err := query.PhotoByUUID(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -186,13 +182,13 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
m.PhotoFavorite = false
|
||||
m.PhotoQuality = m.QualityScore()
|
||||
conf.Db().Save(&m)
|
||||
entity.Db().Save(&m)
|
||||
|
||||
event.Publish("count.favorites", event.Data{
|
||||
"count": -1,
|
||||
})
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, id, c, q)
|
||||
PublishPhotoEvent(EntityUpdated, id, c)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
@@ -209,23 +205,20 @@ func SetPhotoPrimary(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
db := conf.Db()
|
||||
|
||||
uuid := c.Param("uuid")
|
||||
fileUUID := c.Param("file_uuid")
|
||||
q := query.New(db)
|
||||
err := q.SetPhotoPrimary(uuid, fileUUID)
|
||||
err := query.SetPhotoPrimary(uuid, fileUUID)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, uuid, c, q)
|
||||
PublishPhotoEvent(EntityUpdated, uuid, c)
|
||||
|
||||
event.Success("photo saved")
|
||||
|
||||
p, err := q.PreloadPhotoByUUID(uuid)
|
||||
p, err := query.PreloadPhotoByUUID(uuid)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
@@ -25,9 +24,7 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
m, err := q.PhotoByUUID(c.Param("uuid"))
|
||||
db := conf.Db()
|
||||
m, err := query.PhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -55,14 +52,14 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
plm.Uncertainty = f.Uncertainty
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
db.Save(&lm)
|
||||
entity.Db().Save(&lm)
|
||||
|
||||
p, err := q.PreloadPhotoByUUID(c.Param("uuid"))
|
||||
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -74,7 +71,7 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c, q)
|
||||
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
|
||||
|
||||
event.Success("label updated")
|
||||
|
||||
@@ -94,9 +91,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
db := conf.Db()
|
||||
q := query.New(db)
|
||||
m, err := q.PhotoByUUID(c.Param("uuid"))
|
||||
m, err := query.PhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -110,7 +105,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
label, err := q.PhotoLabel(m.ID, uint(labelId))
|
||||
label, err := query.PhotoLabel(m.ID, uint(labelId))
|
||||
|
||||
if err != nil {
|
||||
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 {
|
||||
db.Delete(&label)
|
||||
entity.Db().Delete(&label)
|
||||
} else {
|
||||
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 {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -136,7 +131,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c, q)
|
||||
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
|
||||
|
||||
event.Success("label removed")
|
||||
|
||||
@@ -158,9 +153,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
// TODO: Code clean-up, simplify
|
||||
|
||||
db := conf.Db()
|
||||
q := query.New(db)
|
||||
m, err := q.PhotoByUUID(c.Param("uuid"))
|
||||
m, err := query.PhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -174,7 +167,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
label, err := q.PhotoLabel(m.ID, uint(labelId))
|
||||
label, err := query.PhotoLabel(m.ID, uint(labelId))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
@@ -191,7 +184,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
p, err := q.PreloadPhotoByUUID(c.Param("uuid"))
|
||||
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
@@ -203,7 +196,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c, q)
|
||||
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
|
||||
|
||||
event.Success("label saved")
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"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/gin-gonic/gin"
|
||||
@@ -36,7 +36,6 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
var f form.PhotoSearch
|
||||
|
||||
q := service.Query()
|
||||
err := c.MustBindWith(&f, binding.Form)
|
||||
|
||||
if err != nil {
|
||||
@@ -44,7 +43,7 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
result, count, err := q.Photos(f)
|
||||
result, count, err := query.Photos(f)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
@@ -31,9 +32,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
db := conf.Db()
|
||||
q := query.New(db)
|
||||
f, err := q.FileByHash(fileHash)
|
||||
f, err := query.FileByHash(fileHash)
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
f.FileMissing = true
|
||||
db.Save(&f)
|
||||
entity.Db().Save(&f)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"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/pkg/fs"
|
||||
)
|
||||
@@ -45,8 +45,7 @@ func GetPreview(router *gin.RouterGroup, conf *config.Config) {
|
||||
f.Count = 12
|
||||
f.Order = "relevance"
|
||||
|
||||
q := service.Query()
|
||||
p, _, err := q.Photos(f)
|
||||
p, _, err := query.Photos(f)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"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/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
@@ -47,8 +47,7 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
q := service.Query()
|
||||
files, err := q.FilesByUUID(f.Photos, 1000, 0)
|
||||
files, err := query.FilesByUUID(f.Photos, 1000, 0)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
||||
|
||||
@@ -162,7 +162,7 @@ func (c *Config) ClientConfig() ClientConfig {
|
||||
Take(&count)
|
||||
|
||||
db.Table("places").
|
||||
Select("(COUNT(*) - 1) AS places").
|
||||
Select("(SUM(photo_count > 0) - 1) AS places").
|
||||
Take(&count)
|
||||
|
||||
type country struct {
|
||||
|
||||
@@ -62,9 +62,9 @@ func (c *Config) InitDb() {
|
||||
}
|
||||
|
||||
// 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.ResetDb(testFixtures)
|
||||
entity.InitTestFixtures()
|
||||
}
|
||||
|
||||
// connectToDatabase establishes a database connection.
|
||||
|
||||
@@ -102,7 +102,7 @@ func NewTestConfig() *Config {
|
||||
log.Fatalf("config: %s", err.Error())
|
||||
}
|
||||
|
||||
c.ResetDb(true)
|
||||
c.ResetDb()
|
||||
|
||||
thumb.Size = c.ThumbSize()
|
||||
thumb.Limit = c.ThumbLimit()
|
||||
|
||||
@@ -20,13 +20,18 @@ func SetDbProvider(provider DbProvider) {
|
||||
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 {
|
||||
return dbProvider.Db()
|
||||
}
|
||||
|
||||
// Db() returns an unscoped database connection.
|
||||
func Unscoped() *gorm.DB {
|
||||
// UnscopedDb returns an unscoped database connection.
|
||||
func UnscopedDb() *gorm.DB {
|
||||
return Db().Unscoped()
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ https://github.com/photoprism/photoprism/wiki/Storage
|
||||
package entity
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
)
|
||||
|
||||
var log = event.Log
|
||||
var resetFixturesOnce sync.Once
|
||||
|
||||
func logError(result *gorm.DB) {
|
||||
if result.Error != nil {
|
||||
@@ -84,27 +86,38 @@ func ResetDb(testFixtures bool) {
|
||||
DropTables()
|
||||
|
||||
// Make sure changes have been written to disk.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
MigrateDb()
|
||||
|
||||
if testFixtures {
|
||||
// Make sure changes have been written to disk.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
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.
|
||||
func InitTestDb(dsn string) *Gorm {
|
||||
if HasDbProvider() {
|
||||
return nil
|
||||
}
|
||||
|
||||
db := &Gorm{
|
||||
Driver: "mysql",
|
||||
Dsn: dsn,
|
||||
}
|
||||
|
||||
SetDbProvider(db)
|
||||
ResetDb(true)
|
||||
InitTestFixtures()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ func TestMain(m *testing.M) {
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type Place struct {
|
||||
LocKeywords string `gorm:"type:varchar(255);"`
|
||||
LocNotes string `gorm:"type:text;"`
|
||||
LocFavorite bool
|
||||
PhotoCount int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
New bool `gorm:"-"`
|
||||
@@ -33,6 +34,7 @@ var UnknownPlace = Place{
|
||||
LocKeywords: "",
|
||||
LocNotes: "",
|
||||
LocFavorite: false,
|
||||
PhotoCount: -1,
|
||||
}
|
||||
|
||||
// CreateUnknownPlace initializes default place in the database
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateUnknownPlace(t *testing.T) {
|
||||
@@ -32,7 +33,20 @@ func TestPlace_Find(t *testing.T) {
|
||||
assert.Nil(t, r)
|
||||
})
|
||||
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()
|
||||
assert.Equal(t, "record not found", r.Error())
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package event
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func PublishEntities(name, ev string, entities interface{}) {
|
||||
SharedHub().Publish(Message{
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"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.
|
||||
func (imp *Import) Start(opt ImportOptions) {
|
||||
func (imp *Import) Start(opt ImportOptions) map[string]bool {
|
||||
var directories []string
|
||||
done := make(map[string]bool)
|
||||
ind := imp.index
|
||||
@@ -50,19 +51,19 @@ func (imp *Import) Start(opt ImportOptions) {
|
||||
|
||||
if !fs.PathExists(importPath) {
|
||||
event.Error(fmt.Sprintf("import: %s does not exist", importPath))
|
||||
return
|
||||
return done
|
||||
}
|
||||
|
||||
if err := mutex.Worker.Start(); err != nil {
|
||||
event.Error(fmt.Sprintf("import: %s", err.Error()))
|
||||
return
|
||||
return done
|
||||
}
|
||||
|
||||
defer mutex.Worker.Stop()
|
||||
|
||||
if err := ind.tensorFlow.Init(); err != nil {
|
||||
log.Errorf("import: %s", err.Error())
|
||||
return
|
||||
return done
|
||||
}
|
||||
|
||||
jobs := make(chan ImportJob)
|
||||
@@ -194,7 +195,13 @@ func (imp *Import) Start(opt ImportOptions) {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := query.UpdatePhotoCounts(); err != nil {
|
||||
log.Errorf("import: %s", err)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
|
||||
return done
|
||||
}
|
||||
|
||||
// Cancel stops the current import operation.
|
||||
|
||||
@@ -171,6 +171,10 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := query.UpdatePhotoCounts(); err != nil {
|
||||
log.Errorf("index: %s", err)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
|
||||
return done
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/meta"
|
||||
"github.com/photoprism/photoprism/internal/nsfw"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
@@ -426,7 +427,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,21 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log = logrus.StandardLogger()
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
|
||||
db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DSN"))
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -64,13 +64,11 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
|
||||
runtime.GC()
|
||||
}()
|
||||
|
||||
q := query.New(prg.conf.Db())
|
||||
|
||||
limit := 500
|
||||
offset := 0
|
||||
|
||||
for {
|
||||
files, err := q.ExistingFiles(limit, offset, opt.Path)
|
||||
files, err := query.ExistingFiles(limit, offset, opt.Path)
|
||||
|
||||
if err != nil {
|
||||
return purgedFiles, purgedPhotos, err
|
||||
@@ -118,7 +116,7 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
|
||||
offset = 0
|
||||
|
||||
for {
|
||||
photos, err := q.MissingPhotos(limit, offset)
|
||||
photos, err := query.MissingPhotos(limit, offset)
|
||||
|
||||
if err != nil {
|
||||
return purgedFiles, purgedPhotos, err
|
||||
@@ -163,11 +161,17 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
|
||||
offset += limit
|
||||
}
|
||||
|
||||
err = q.ResetPhotosQuality()
|
||||
|
||||
if err := query.ResetPhotosQuality(); err != nil {
|
||||
return purgedFiles, purgedPhotos, err
|
||||
}
|
||||
|
||||
if err := query.UpdatePhotoCounts(); err != nil {
|
||||
return purgedFiles, purgedPhotos, err
|
||||
}
|
||||
|
||||
return purgedFiles, purgedPhotos, nil
|
||||
}
|
||||
|
||||
// Cancel stops the current purge operation.
|
||||
func (prg *Purge) Cancel() {
|
||||
mutex.Worker.Cancel()
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
)
|
||||
|
||||
// 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) {
|
||||
s := q.db
|
||||
|
||||
s = s.Where("files.file_missing = 0").
|
||||
func AccountUploads(a entity.Account, limit int) (results []entity.File, err error) {
|
||||
s := Db().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)
|
||||
|
||||
if !a.SyncRaw {
|
||||
|
||||
@@ -5,19 +5,13 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
)
|
||||
|
||||
func TestQuery_AccountUploads(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
q := New(conf.Db())
|
||||
|
||||
func TestAccountUploads(t *testing.T) {
|
||||
a := entity.Account{ID: 1, SyncRaw: false}
|
||||
|
||||
t.Run("find uploads", func(t *testing.T) {
|
||||
results, err := q.AccountUploads(a, 10)
|
||||
results, err := AccountUploads(a, 10)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
)
|
||||
|
||||
// Accounts returns a list of accounts.
|
||||
func (q *Query) Accounts(f form.AccountSearch) (result []entity.Account, err error) {
|
||||
s := q.db.Where(&entity.Account{})
|
||||
func Accounts(f form.AccountSearch) (result []entity.Account, err error) {
|
||||
s := Db().Where(&entity.Account{})
|
||||
|
||||
if f.Share {
|
||||
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.
|
||||
func (q *Query) AccountByID(id uint) (result entity.Account, err error) {
|
||||
if err := q.db.Where("id = ?", id).First(&result).Error; err != nil {
|
||||
func AccountByID(id uint) (result entity.Account, err error) {
|
||||
if err := Db().Where("id = ?", id).First(&result).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ type AlbumResult struct {
|
||||
}
|
||||
|
||||
// AlbumByUUID returns a Album based on the UUID.
|
||||
func (q *Query) AlbumByUUID(albumUUID string) (album entity.Album, err error) {
|
||||
if err := q.db.Where("album_uuid = ?", albumUUID).Preload("Links").First(&album).Error; err != nil {
|
||||
func AlbumByUUID(albumUUID string) (album entity.Album, err error) {
|
||||
if err := Db().Where("album_uuid = ?", albumUUID).Preload("Links").First(&album).Error; err != nil {
|
||||
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.
|
||||
func (q *Query) AlbumThumbByUUID(albumUUID string) (file entity.File, err error) {
|
||||
if err := q.db.Where("files.file_primary = 1 AND files.deleted_at IS NULL").
|
||||
func AlbumThumbByUUID(albumUUID string) (file entity.File, err error) {
|
||||
if err := Db().Where("files.file_primary = 1 AND files.deleted_at IS NULL").
|
||||
Joins("JOIN albums ON albums.album_uuid = ?", albumUUID).
|
||||
Joins("JOIN photos_albums pa ON pa.album_uuid = albums.album_uuid AND pa.photo_uuid = files.photo_uuid").
|
||||
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.
|
||||
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 {
|
||||
return results, err
|
||||
}
|
||||
|
||||
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").
|
||||
Select(`albums.*,
|
||||
@@ -1,20 +1,15 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
form "github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestAlbumByUUID(t *testing.T) {
|
||||
t.Run("existing uuid", func(t *testing.T) {
|
||||
album, err := search.AlbumByUUID("at9lxuqxpogaaba7")
|
||||
album, err := AlbumByUUID("at9lxuqxpogaaba7")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -24,19 +19,15 @@ func TestQuery_AlbumByUUID(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")
|
||||
t.Log(album)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_AlbumThumbByUUID(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestAlbumThumbByUUID(t *testing.T) {
|
||||
t.Run("existing uuid", func(t *testing.T) {
|
||||
file, err := search.AlbumThumbByUUID("at9lxuqxpogaaba8")
|
||||
file, err := AlbumThumbByUUID("at9lxuqxpogaaba8")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -46,20 +37,16 @@ func TestQuery_AlbumThumbByUUID(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")
|
||||
t.Log(file)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_Albums(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestAlbums(t *testing.T) {
|
||||
t.Run("search with string", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("chr")
|
||||
result, err := search.Albums(query)
|
||||
result, err := Albums(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -70,7 +57,7 @@ func TestQuery_Albums(t *testing.T) {
|
||||
|
||||
t.Run("search with slug", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("slug:holiday count:10")
|
||||
result, err := search.Albums(query)
|
||||
result, err := Albums(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -82,7 +69,7 @@ func TestQuery_Albums(t *testing.T) {
|
||||
t.Run("favorites true", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("favorites:true count:10000")
|
||||
|
||||
result, err := search.Albums(query)
|
||||
result, err := Albums(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -93,7 +80,7 @@ func TestQuery_Albums(t *testing.T) {
|
||||
t.Run("empty query", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("order:slug")
|
||||
|
||||
result, err := search.Albums(query)
|
||||
result, err := Albums(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -103,7 +90,7 @@ func TestQuery_Albums(t *testing.T) {
|
||||
})
|
||||
t.Run("search with invalid query string", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("xxx:bla")
|
||||
result, err := search.Albums(query)
|
||||
result, err := Albums(query)
|
||||
assert.Error(t, err, "unknown filter")
|
||||
t.Log(result)
|
||||
})
|
||||
@@ -9,8 +9,8 @@ type CategoryLabel struct {
|
||||
Title string
|
||||
}
|
||||
|
||||
func (q *Query) CategoryLabels(limit, offset int) (results []CategoryLabel) {
|
||||
s := q.db.NewScope(nil).DB()
|
||||
func CategoryLabels(limit, offset int) (results []CategoryLabel) {
|
||||
s := Db().NewScope(nil).DB()
|
||||
|
||||
s = s.Table("categories").
|
||||
Select("label_name AS name").
|
||||
13
internal/query/categories_test.go
Normal file
13
internal/query/categories_test.go
Normal 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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
18
internal/query/counts.go
Normal 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
|
||||
}
|
||||
13
internal/query/counts_test.go
Normal file
13
internal/query/counts_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUpdatePhotoCounts(t *testing.T) {
|
||||
err := UpdatePhotoCounts()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
)
|
||||
|
||||
// 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) {
|
||||
s := q.db.Where(&entity.FileShare{})
|
||||
func FileShares(accountId uint, status string) (result []entity.FileShare, err error) {
|
||||
s := Db().Where(&entity.FileShare{})
|
||||
|
||||
if accountId > 0 {
|
||||
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.
|
||||
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 {
|
||||
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)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
result := q.db.Model(entity.FileSync{}).
|
||||
result := Db().Model(entity.FileSync{}).
|
||||
Where("remote_name = ? AND status = ? AND file_id = 0", filename, entity.FileSyncDownloaded).
|
||||
Update("file_id", fileId)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
)
|
||||
|
||||
// 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) {
|
||||
s := q.db.Where(&entity.FileSync{})
|
||||
func FileSyncs(accountId uint, status string, limit int) (result []entity.FileSync, err error) {
|
||||
s := Db().Where(&entity.FileSync{})
|
||||
|
||||
if accountId > 0 {
|
||||
s = s.Where("account_id = ?", accountId)
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
)
|
||||
|
||||
// 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, "/") {
|
||||
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 != "" {
|
||||
stmt = stmt.Where("file_name LIKE ?", filePath+"/%")
|
||||
@@ -24,8 +24,8 @@ func (q *Query) ExistingFiles(limit int, offset int, filePath string) (files []e
|
||||
}
|
||||
|
||||
// FilesByUUID
|
||||
func (q *Query) 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 {
|
||||
func FilesByUUID(u []string, limit int, offset int) (files []entity.File, err error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ func (q *Query) FilesByUUID(u []string, limit int, offset int) (files []entity.F
|
||||
}
|
||||
|
||||
// FileByPhotoUUID
|
||||
func (q *Query) 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 {
|
||||
func FileByPhotoUUID(u string) (file entity.File, err error) {
|
||||
if err := Db().Where("photo_uuid = ? AND file_primary = 1", u).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||
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.
|
||||
func (q *Query) 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 {
|
||||
func FileByUUID(uuid string) (file entity.File, err error) {
|
||||
if err := Db().Where("file_uuid = ?", uuid).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||
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.
|
||||
func (q *Query) 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 {
|
||||
func FileByHash(fileHash string) (file entity.File, err error) {
|
||||
if err := Db().Where("file_hash = ?", fileHash).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||
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.
|
||||
func (q *Query) SetPhotoPrimary(photoUUID, fileUUID string) error {
|
||||
q.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
|
||||
func SetPhotoPrimary(photoUUID, fileUUID string) error {
|
||||
Db().Model(entity.File{}).Where("photo_uuid = ? AND file_uuid <> ?", photoUUID, fileUUID).UpdateColumn("file_primary", false)
|
||||
return Db().Model(entity.File{}).Where("photo_uuid = ? AND file_uuid = ?", photoUUID, fileUUID).UpdateColumn("file_primary", true).Error
|
||||
}
|
||||
@@ -1,32 +1,25 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestQuery_ExistingFiles(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestExistingFiles(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.LessOrEqual(t, 5, len(files))
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_FilesByUUID(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestFilesByUUID(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.Equal(t, 1, len(files))
|
||||
@@ -34,33 +27,25 @@ func TestQuery_FilesByUUID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_FileByPhotoUUID(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestFileByPhotoUUID(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.Equal(t, "exampleDNGFile.dng", file.FileName)
|
||||
})
|
||||
|
||||
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")
|
||||
t.Log(file)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_FileByUUID(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestFileByUUID(t *testing.T) {
|
||||
t.Run("files found", func(t *testing.T) {
|
||||
file, err := search.FileByUUID("ft8es39w45bnlqdw")
|
||||
file, err := FileByUUID("ft8es39w45bnlqdw")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -70,7 +55,7 @@ func TestQuery_FileByUUID(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("no files found", func(t *testing.T) {
|
||||
file, err := search.FileByUUID("111")
|
||||
file, err := FileByUUID("111")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("error expected")
|
||||
@@ -81,20 +66,16 @@ func TestQuery_FileByUUID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_FileByHash(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestFileByHash(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.Equal(t, "exampleFileName.jpg", file.FileName)
|
||||
})
|
||||
|
||||
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")
|
||||
t.Log(file)
|
||||
@@ -12,37 +12,15 @@ import (
|
||||
"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.
|
||||
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 {
|
||||
return results, err
|
||||
}
|
||||
|
||||
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("search: %+v", f)))
|
||||
|
||||
s := q.db.NewScope(nil).DB()
|
||||
s := UnscopedDb()
|
||||
|
||||
s = s.Table("photos").
|
||||
Select(`photos.id, photos.photo_uuid, photos.photo_lat, photos.photo_lng, photos.photo_title,
|
||||
|
||||
27
internal/query/geo_result.go
Normal file
27
internal/query/geo_result.go
Normal 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)
|
||||
}
|
||||
@@ -1,21 +1,16 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestQuery_Geo(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestGeo(t *testing.T) {
|
||||
t.Run("search all photos", func(t *testing.T) {
|
||||
query := form.NewGeoSearch("")
|
||||
result, err := search.Geo(query)
|
||||
result, err := Geo(query)
|
||||
|
||||
assert.Nil(t, err)
|
||||
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) {
|
||||
query := form.NewGeoSearch("Query:bridge Before:3006-01-02")
|
||||
result, err := search.Geo(query)
|
||||
result, err := Geo(query)
|
||||
|
||||
assert.Nil(t, err)
|
||||
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) {
|
||||
query := form.NewGeoSearch("After:2014-12-02 Before:3006-01-02")
|
||||
result, err := search.Geo(query)
|
||||
result, err := Geo(query)
|
||||
|
||||
assert.Nil(t, err)
|
||||
t.Log(result)
|
||||
|
||||
23
internal/query/label_result.go
Normal file
23
internal/query/label_result.go
Normal 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
|
||||
}
|
||||
@@ -12,27 +12,9 @@ import (
|
||||
"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.
|
||||
func (q *Query) 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 {
|
||||
func PhotoLabel(photoID, labelID uint) (label entity.PhotoLabel, err error) {
|
||||
if err := Db().Where("photo_id = ? AND label_id = ?", photoID, labelID).Preload("Photo").Preload("Label").First(&label).Error; err != nil {
|
||||
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.
|
||||
func (q *Query) 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 {
|
||||
func LabelBySlug(labelSlug string) (label entity.Label, err error) {
|
||||
if err := Db().Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).Preload("Links").First(&label).Error; err != nil {
|
||||
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.
|
||||
func (q *Query) LabelByUUID(labelUUID string) (label entity.Label, err error) {
|
||||
if err := q.db.Where("label_uuid = ?", labelUUID).Preload("Links").First(&label).Error; err != nil {
|
||||
func LabelByUUID(labelUUID string) (label entity.Label, err error) {
|
||||
if err := Db().Where("label_uuid = ?", labelUUID).Preload("Links").First(&label).Error; err != nil {
|
||||
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.
|
||||
func (q *Query) LabelThumbBySlug(labelSlug string) (file entity.File, err error) {
|
||||
if err := q.db.Where("files.file_primary AND files.deleted_at IS NULL").
|
||||
func LabelThumbBySlug(labelSlug string) (file entity.File, err error) {
|
||||
if err := Db().Where("files.file_primary AND files.deleted_at IS NULL").
|
||||
Joins("JOIN labels ON labels.label_slug = ?", labelSlug).
|
||||
Joins("JOIN photos_labels ON photos_labels.label_id = labels.id AND photos_labels.photo_id = files.photo_id").
|
||||
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.
|
||||
func (q *Query) LabelThumbByUUID(labelUUID string) (file entity.File, err error) {
|
||||
func LabelThumbByUUID(labelUUID string) (file entity.File, err error) {
|
||||
// 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 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").
|
||||
@@ -86,7 +68,7 @@ func (q *Query) LabelThumbByUUID(labelUUID string) (file entity.File, err error)
|
||||
}
|
||||
|
||||
// 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 categories c ON photos_labels.label_id = c.label_id").
|
||||
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.
|
||||
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 {
|
||||
return results, err
|
||||
}
|
||||
|
||||
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("labels: %+v", f)))
|
||||
|
||||
s := q.db.NewScope(nil).DB()
|
||||
s := UnscopedDb()
|
||||
|
||||
// s.LogMode(true)
|
||||
|
||||
@@ -132,14 +114,14 @@ func (q *Query) Labels(f form.LabelSearch) (results []LabelResult, err error) {
|
||||
slugString := slug.Make(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))
|
||||
|
||||
s = s.Where("LOWER(labels.label_name) LIKE ?", likeString)
|
||||
} else {
|
||||
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 {
|
||||
labelIds = append(labelIds, category.LabelID)
|
||||
@@ -1,21 +1,16 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
)
|
||||
|
||||
func TestQuery_LabelBySlug(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
q := New(conf.Db())
|
||||
|
||||
func TestLabelBySlug(t *testing.T) {
|
||||
t.Run("files found", func(t *testing.T) {
|
||||
label, err := q.LabelBySlug("flower")
|
||||
label, err := LabelBySlug("flower")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -25,20 +20,16 @@ func TestQuery_LabelBySlug(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.Empty(t, label.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_LabelByUUID(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
q := New(conf.Db())
|
||||
|
||||
func TestLabelByUUID(t *testing.T) {
|
||||
t.Run("files found", func(t *testing.T) {
|
||||
label, err := q.LabelByUUID("lt9k3pw1wowuy3c5")
|
||||
label, err := LabelByUUID("lt9k3pw1wowuy3c5")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -48,20 +39,16 @@ func TestQuery_LabelByUUID(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.Empty(t, label.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_LabelThumbBySlug(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
q := New(conf.Db())
|
||||
|
||||
func TestLabelThumbBySlug(t *testing.T) {
|
||||
t.Run("files found", func(t *testing.T) {
|
||||
file, err := q.LabelThumbBySlug("flower")
|
||||
file, err := LabelThumbBySlug("flower")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -71,20 +58,16 @@ func TestQuery_LabelThumbBySlug(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")
|
||||
t.Log(file)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_LabelThumbByUUID(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
q := New(conf.Db())
|
||||
|
||||
func TestLabelThumbByUUID(t *testing.T) {
|
||||
t.Run("files found", func(t *testing.T) {
|
||||
file, err := q.LabelThumbByUUID("lt9k3pw1wowuy3c4")
|
||||
file, err := LabelThumbByUUID("lt9k3pw1wowuy3c4")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -94,20 +77,17 @@ func TestQuery_LabelThumbByUUID(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")
|
||||
t.Log(file)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_Labels(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
q := New(conf.Db())
|
||||
|
||||
func TestLabels(t *testing.T) {
|
||||
t.Run("search with query", func(t *testing.T) {
|
||||
query := form.NewLabelSearch("Query:C Count:1005 Order:slug")
|
||||
result, err := q.Labels(query)
|
||||
result, err := Labels(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -134,7 +114,7 @@ func TestQuery_Labels(t *testing.T) {
|
||||
|
||||
t.Run("search for favorites", func(t *testing.T) {
|
||||
query := form.NewLabelSearch("Favorites:true")
|
||||
result, err := q.Labels(query)
|
||||
result, err := Labels(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -160,7 +140,7 @@ func TestQuery_Labels(t *testing.T) {
|
||||
|
||||
t.Run("search with empty query", func(t *testing.T) {
|
||||
query := form.NewLabelSearch("")
|
||||
result, err := q.Labels(query)
|
||||
result, err := Labels(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -171,7 +151,7 @@ func TestQuery_Labels(t *testing.T) {
|
||||
|
||||
t.Run("search with invalid query string", func(t *testing.T) {
|
||||
query := form.NewLabelSearch("xxx:bla")
|
||||
result, err := q.Labels(query)
|
||||
result, err := Labels(query)
|
||||
|
||||
assert.Error(t, err, "unknown filter")
|
||||
assert.Empty(t, result)
|
||||
@@ -8,8 +8,8 @@ type MomentsTimeResult struct {
|
||||
}
|
||||
|
||||
// GetMomentsTime counts photos per month and year
|
||||
func (q *Query) GetMomentsTime() (results []MomentsTimeResult, err error) {
|
||||
s := q.db.NewScope(nil).DB()
|
||||
func GetMomentsTime() (results []MomentsTimeResult, err error) {
|
||||
s := UnscopedDb()
|
||||
|
||||
s = s.Table("photos").
|
||||
Where("deleted_at IS NULL").
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestQuery_GetMomentsTime(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestGetMomentsTime(t *testing.T) {
|
||||
t.Run("result found", func(t *testing.T) {
|
||||
result, err := search.GetMomentsTime()
|
||||
result, err := GetMomentsTime()
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2790, result[0].PhotoYear)
|
||||
|
||||
@@ -1,407 +1,13 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/jinzhu/gorm"
|
||||
"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.
|
||||
func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) {
|
||||
if err := q.db.Unscoped().Where("id = ?", photoID).
|
||||
func PhotoByID(photoID uint64) (photo entity.Photo, err error) {
|
||||
if err := UnscopedDb().Where("id = ?", photoID).
|
||||
Preload("Links").
|
||||
Preload("Description").
|
||||
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.
|
||||
func (q *Query) PhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
|
||||
if err := q.db.Unscoped().Where("photo_uuid = ?", photoUUID).
|
||||
func PhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
|
||||
if err := UnscopedDb().Where("photo_uuid = ?", photoUUID).
|
||||
Preload("Links").
|
||||
Preload("Description").
|
||||
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.
|
||||
func (q *Query) PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
|
||||
if err := q.db.Unscoped().Where("photo_uuid = ?", photoUUID).
|
||||
func PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
|
||||
if err := UnscopedDb().Where("photo_uuid = ?", photoUUID).
|
||||
Preload("Labels", func(db *gorm.DB) *gorm.DB {
|
||||
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.
|
||||
func (q *Query) MissingPhotos(limit int, offset int) (entities []entity.Photo, err error) {
|
||||
err = q.db.
|
||||
func MissingPhotos(limit int, offset int) (entities []entity.Photo, err error) {
|
||||
err = Db().
|
||||
Select("photos.*").
|
||||
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").
|
||||
@@ -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.
|
||||
func (q *Query) ResetPhotosQuality() error {
|
||||
return q.db.Table("photos").
|
||||
func ResetPhotosQuality() error {
|
||||
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)").
|
||||
Update("photo_quality", -1).Error
|
||||
}
|
||||
|
||||
134
internal/query/photo_results.go
Normal file
134
internal/query/photo_results.go
Normal 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
|
||||
}
|
||||
@@ -4,9 +4,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
||||
/*func TestQuery_Photos(t *testing.T) {
|
||||
@@ -28,324 +25,44 @@ import (
|
||||
})
|
||||
}*/
|
||||
|
||||
func TestQuery_PhotoByID(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestPhotoByID(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.Equal(t, 2790, result.PhotoYear)
|
||||
})
|
||||
|
||||
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")
|
||||
t.Log(result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_PhotoByUUID(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestPhotoByUUID(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.Equal(t, "Reunion", result.PhotoTitle)
|
||||
})
|
||||
|
||||
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")
|
||||
t.Log(result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery_PreloadPhotoByUUID(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
search := New(conf.Db())
|
||||
|
||||
func TestPreloadPhotoByUUID(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.Equal(t, "Reunion", result.PhotoTitle)
|
||||
})
|
||||
|
||||
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")
|
||||
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
275
internal/query/photos.go
Normal 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
|
||||
}
|
||||
271
internal/query/photos_test.go
Normal file
271
internal/query/photos_test.go
Normal 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)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ https://github.com/photoprism/photoprism/wiki
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
@@ -36,3 +37,13 @@ func New(db *gorm.DB) *Query {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -11,11 +12,13 @@ func TestMain(m *testing.M) {
|
||||
log = logrus.StandardLogger()
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
|
||||
// db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DSN"))
|
||||
db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DSN"))
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// db.Close()
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
)
|
||||
|
||||
// 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() {
|
||||
return results, errors.New("no photos selected")
|
||||
}
|
||||
|
||||
s := q.db.NewScope(nil).DB()
|
||||
s := Db().NewScope(nil).DB()
|
||||
|
||||
s = s.Table("photos").
|
||||
Select("photos.*").
|
||||
|
||||
@@ -39,11 +39,8 @@ func (s *Share) Start() (err error) {
|
||||
Share: true,
|
||||
}
|
||||
|
||||
db := s.conf.Db()
|
||||
q := query.New(db)
|
||||
|
||||
// Find accounts for which sharing is enabled
|
||||
accounts, err := q.Accounts(f)
|
||||
accounts, err := query.Accounts(f)
|
||||
|
||||
// Upload newly shared files
|
||||
for _, a := range accounts {
|
||||
@@ -55,7 +52,7 @@ func (s *Share) Start() (err error) {
|
||||
continue
|
||||
}
|
||||
|
||||
files, err := q.FileShares(a.ID, entity.FileShareNew)
|
||||
files, err := query.FileShares(a.ID, entity.FileShareNew)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("share: %s", err.Error())
|
||||
@@ -121,7 +118,7 @@ func (s *Share) Start() (err error) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -137,7 +134,7 @@ func (s *Share) Start() (err error) {
|
||||
continue
|
||||
}
|
||||
|
||||
files, err := q.ExpiredFileShares(a)
|
||||
files, err := query.ExpiredFileShares(a)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("share: %s", err.Error())
|
||||
@@ -166,7 +163,7 @@ func (s *Share) Start() (err error) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,12 @@ import (
|
||||
// Sync represents a sync worker.
|
||||
type Sync struct {
|
||||
conf *config.Config
|
||||
q *query.Query
|
||||
}
|
||||
|
||||
// NewSync returns a new service sync worker.
|
||||
func NewSync(conf *config.Config) *Sync {
|
||||
return &Sync{
|
||||
conf: conf,
|
||||
q: query.New(conf.Db()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +38,7 @@ func (s *Sync) Start() (err error) {
|
||||
Sync: true,
|
||||
}
|
||||
|
||||
db := s.conf.Db()
|
||||
q := s.q
|
||||
|
||||
accounts, err := q.Accounts(f)
|
||||
accounts, err := query.Accounts(f)
|
||||
|
||||
for _, a := range accounts {
|
||||
if a.AccType != remote.ServiceWebDAV {
|
||||
@@ -117,7 +112,7 @@ func (s *Sync) Start() (err error) {
|
||||
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())
|
||||
return err
|
||||
}
|
||||
@@ -128,7 +123,7 @@ func (s *Sync) Start() (err error) {
|
||||
a.SyncStatus = syncStatus
|
||||
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())
|
||||
} else if synced {
|
||||
event.Publish("sync.synced", event.Data{"account": a})
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/remote/webdav"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
@@ -20,12 +21,13 @@ func (s *Sync) downloadPath() string {
|
||||
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) {
|
||||
result = make(Downloads)
|
||||
maxResults := 1000
|
||||
|
||||
// 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 {
|
||||
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
|
||||
func (s *Sync) download(a entity.Account) (complete bool, err error) {
|
||||
db := s.conf.Db()
|
||||
|
||||
// Set up index worker
|
||||
indexJobs := make(chan photoprism.IndexJob)
|
||||
|
||||
go photoprism.IndexWorker(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())
|
||||
} else {
|
||||
files[i] = file
|
||||
|
||||
@@ -8,17 +8,16 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/remote/webdav"
|
||||
)
|
||||
|
||||
// Uploads local files to a remote account
|
||||
func (s *Sync) upload(a entity.Account) (complete bool, err error) {
|
||||
db := s.conf.Db()
|
||||
q := s.q
|
||||
maxResults := 250
|
||||
|
||||
// Get upload file list from database
|
||||
files, err := q.AccountUploads(a, maxResults)
|
||||
files, err := query.AccountUploads(a, maxResults)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -68,7 +67,7 @@ func (s *Sync) upload(a entity.Account) (complete bool, err error) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user