diff --git a/assets/config/labels.yml b/assets/config/labels.yml index 4523eb75f..8444ff663 100644 --- a/assets/config/labels.yml +++ b/assets/config/labels.yml @@ -1610,6 +1610,7 @@ zebra: categories: - animal - mammal + - wildlife hog: threshold: 0.66 @@ -1629,11 +1630,13 @@ warthog: categories: - animal - mammal + - wildlife hippopotamus: label: hippo categories: - animal + - wildlife ox: label: cow @@ -1743,10 +1746,10 @@ three-toed sloth: - animal ape: + label: ape priority: 2 categories: - animal - - ape - mammal orangutan: @@ -1782,6 +1785,7 @@ baboon: categories: - animal - monkey + - wildlife macaque: see: monkey diff --git a/frontend/src/model/label.js b/frontend/src/model/label.js index a919ca745..57307a979 100644 --- a/frontend/src/model/label.js +++ b/frontend/src/model/label.js @@ -8,7 +8,7 @@ class Label extends Abstract { } getId() { - return this.LabelSlug; + return this.LabelUUID; } getTitle() { diff --git a/frontend/src/pages/album/photos.vue b/frontend/src/pages/album/photos.vue index eb453fa1b..6ace8b968 100644 --- a/frontend/src/pages/album/photos.vue +++ b/frontend/src/pages/album/photos.vue @@ -54,11 +54,11 @@ const query = this.$route.query; this.uuid = this.$route.params.uuid; - this.filter.q = query['q']; + this.filter.q = query['q'] ? query['q'] : ''; this.filter.camera = query['camera'] ? parseInt(query['camera']) : 0; this.filter.country = query['country'] ? query['country'] : ''; + this.settings.view = this.viewType(); this.lastFilter = {}; - this.loading = true; this.routeName = this.$route.name; this.findAlbum(); this.search(); @@ -94,10 +94,10 @@ methods: { viewType() { let queryParam = this.$route.query['view']; - let storedType = window.localStorage.getItem("photo_view_type"); + let storedType = window.localStorage.getItem("album_view_type"); if (queryParam) { - window.localStorage.setItem("photo_view_type", queryParam); + window.localStorage.setItem("album_view_type", queryParam); return queryParam; } else if (storedType) { return storedType; @@ -206,6 +206,7 @@ Object.assign(this.lastFilter, this.filter); this.offset = 0; + this.loading = true; const params = this.searchParams(); diff --git a/frontend/src/pages/albums.vue b/frontend/src/pages/albums.vue index 24a1e60a4..88658fed7 100644 --- a/frontend/src/pages/albums.vue +++ b/frontend/src/pages/albums.vue @@ -7,6 +7,7 @@ + + refresh + + add - + + + + - +
@@ -134,7 +142,7 @@ '$route'() { const query = this.$route.query; - this.filter.q = query['q']; + this.filter.q = query['q'] ? query['q'] : ''; this.lastFilter = {}; this.routeName = this.$route.name; this.search(); @@ -149,6 +157,7 @@ return { results: [], + loading: true, scrollDisabled: true, pageSize: 24, offset: 0, @@ -210,6 +219,10 @@ } } + if (JSON.stringify(this.$route.query) === JSON.stringify(query)) { + return + } + this.$router.replace({query: query}); }, searchParams() { @@ -238,10 +251,12 @@ Object.assign(this.lastFilter, this.filter); this.offset = 0; + this.loading = true; const params = this.searchParams(); Album.search(params).then(response => { + this.loading = false; this.results = response.models; this.scrollDisabled = (response.models.length < this.pageSize); @@ -259,7 +274,7 @@ this.$nextTick(() => this.$emit("scrollRefresh")); } - }); + }).catch(() => this.loading = false); }, refresh() { this.lastFilter = {}; diff --git a/frontend/src/pages/labels.vue b/frontend/src/pages/labels.vue index 3f5c4feac..4611f2ded 100644 --- a/frontend/src/pages/labels.vue +++ b/frontend/src/pages/labels.vue @@ -8,6 +8,7 @@ single-line :label="labels.search" prepend-inner-icon="search" + browser-autocomplete="off" clearable color="secondary-dark" @click:clear="clearQuery" @@ -17,14 +18,21 @@ > + + + refresh + - + + + + - - + +

No labels matched your search

@@ -89,7 +97,7 @@ '$route'() { const query = this.$route.query; - this.filter.q = query['q']; + this.filter.q = query['q'] ? query['q'] : ''; this.lastFilter = {}; this.routeName = this.$route.name; this.search(); @@ -105,6 +113,7 @@ return { results: [], scrollDisabled: true, + loading: true, pageSize: 24, offset: 0, selection: this.$clipboard.selection, @@ -120,7 +129,7 @@ methods: { clearQuery() { this.filter.q = ''; - this.search(); + this.updateQuery(); }, openLabel(index) { const label = this.results[index]; @@ -163,6 +172,10 @@ } } + if (JSON.stringify(this.$route.query) === JSON.stringify(query)) { + return + } + this.$router.replace({query: query}); }, searchParams() { @@ -179,6 +192,14 @@ return params; }, + refresh() { + this.lastFilter = {}; + const pageSize = this.pageSize; + this.pageSize = this.offset + pageSize; + this.search(); + this.offset = this.pageSize; + this.pageSize = pageSize; + }, search() { this.scrollDisabled = true; @@ -191,10 +212,13 @@ Object.assign(this.lastFilter, this.filter); this.offset = 0; + this.loading = true; const params = this.searchParams(); Label.search(params).then(response => { + this.loading = false; + this.results = response.models; this.scrollDisabled = (response.models.length < this.pageSize); @@ -206,7 +230,7 @@ this.$nextTick(() => this.$emit("scrollRefresh")); } - }); + }).catch(() => this.loading = false); }, }, created() { diff --git a/frontend/src/pages/photos.vue b/frontend/src/pages/photos.vue index da1e57d53..f13b95060 100644 --- a/frontend/src/pages/photos.vue +++ b/frontend/src/pages/photos.vue @@ -47,13 +47,13 @@ '$route'() { const query = this.$route.query; - this.filter.q = query['q']; + this.filter.q = query['q'] ? query['q'] : ''; this.filter.camera = query['camera'] ? parseInt(query['camera']) : 0; this.filter.country = query['country'] ? query['country'] : ''; this.filter.before = query['before'] ? query['before'] : ''; this.filter.after = query['after'] ? query['after'] : ''; + this.settings.view = this.viewType(); this.lastFilter = {}; - this.loading = true; this.routeName = this.$route.name; this.search(); } @@ -197,6 +197,7 @@ Object.assign(this.lastFilter, this.filter); this.offset = 0; + this.loading = true; const params = this.searchParams(); diff --git a/frontend/tests/unit/model/label_test.js b/frontend/tests/unit/model/label_test.js index 33937afa4..c3ad257bd 100644 --- a/frontend/tests/unit/model/label_test.js +++ b/frontend/tests/unit/model/label_test.js @@ -13,49 +13,49 @@ mock describe("model/label", () => { it("should get label entity name", () => { - const values = {id: 5, LabelName: "Black Cat", LabelSlug: "black-cat"}; + const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"}; const label = new Label(values); const result = label.getEntityName(); assert.equal(result, "black-cat"); }); it("should get label id", () => { - const values = {id: 5, LabelName: "Black Cat", LabelSlug: "black-cat"}; + const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"}; const label = new Label(values); const result = label.getId(); - assert.equal(result, "black-cat"); + assert.equal(result, "ABC123"); }); it("should get label title", () => { - const values = {id: 5, LabelName: "Black Cat", LabelSlug: "black-cat"}; + const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"}; const label = new Label(values); const result = label.getTitle(); assert.equal(result, "Black Cat"); }); it("should get thumbnail url", () => { - const values = {id: 5, LabelName: "Black Cat", LabelSlug: "black-cat"}; + const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"}; const label = new Label(values); const result = label.getThumbnailUrl("xyz"); - assert.equal(result, "/api/v1/labels/black-cat/thumbnail/xyz"); + assert.equal(result, "/api/v1/labels/ABC123/thumbnail/xyz"); }); it("should get thumbnail src set", () => { - const values = {id: 5, LabelName: "Black Cat", LabelSlug: "black-cat"}; + const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"}; const label = new Label(values); const result = label.getThumbnailSrcset(""); - assert.equal(result, "/api/v1/labels/black-cat/thumbnail/fit_720 720w, /api/v1/labels/black-cat/thumbnail/fit_1280 1280w, /api/v1/labels/black-cat/thumbnail/fit_1920 1920w, /api/v1/labels/black-cat/thumbnail/fit_2560 2560w, /api/v1/labels/black-cat/thumbnail/fit_3840 3840w"); + assert.equal(result, "/api/v1/labels/ABC123/thumbnail/fit_720 720w, /api/v1/labels/ABC123/thumbnail/fit_1280 1280w, /api/v1/labels/ABC123/thumbnail/fit_1920 1920w, /api/v1/labels/ABC123/thumbnail/fit_2560 2560w, /api/v1/labels/ABC123/thumbnail/fit_3840 3840w"); }); it("should get thumbnail sizes", () => { - const values = {id: 5, LabelName: "Black Cat", LabelSlug: "black-cat"}; + const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat"}; const label = new Label(values); const result = label.getThumbnailSizes(); assert.equal(result, "(min-width: 2560px) 3840px, (min-width: 1920px) 2560px, (min-width: 1280px) 1920px, (min-width: 720px) 1280px, 720px"); }); it("should get date string", () => { - const values = {ID: 5, LabelName: "Black Cat", LabelSlug: "black-cat", CreatedAt: "2012-07-08T14:45:39Z"}; + const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat", CreatedAt: "2012-07-08T14:45:39Z"}; const label = new Label(values); const result = label.getDateString(); assert.equal(result, "Jul 8, 2012, 2:45 PM"); @@ -72,7 +72,7 @@ describe("model/label", () => { }); it("should like label", () => { - const values = {ID: 5, LabelName: "Black Cat", LabelSlug: "black-cat", LabelFavorite: false}; + const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat", LabelFavorite: false}; const label = new Label(values); assert.equal(label.LabelFavorite, false); label.like(); @@ -80,7 +80,7 @@ describe("model/label", () => { }); it("should unlike label", () => { - const values = {ID: 5, LabelName: "Black Cat", LabelSlug: "black-cat", LabelFavorite: true}; + const values = {ID: 5, LabelUUID: "ABC123",LabelName: "Black Cat", LabelSlug: "black-cat", LabelFavorite: true}; const label = new Label(values); assert.equal(label.LabelFavorite, true); label.unlike(); @@ -88,7 +88,7 @@ describe("model/label", () => { }); it("should toggle like", () => { - const values = {ID: 5, LabelName: "Black Cat", LabelSlug: "black-cat", LabelFavorite: true}; + const values = {ID: 5, LabelUUID: "ABC123", LabelName: "Black Cat", LabelSlug: "black-cat", LabelFavorite: true}; const label = new Label(values); assert.equal(label.LabelFavorite, true); label.toggleLike(); diff --git a/internal/api/albums.go b/internal/api/album.go similarity index 100% rename from internal/api/albums.go rename to internal/api/album.go diff --git a/internal/api/albums_test.go b/internal/api/album_test.go similarity index 100% rename from internal/api/albums_test.go rename to internal/api/album_test.go diff --git a/internal/api/labels.go b/internal/api/label.go similarity index 66% rename from internal/api/labels.go rename to internal/api/label.go index f64325029..78dbba72c 100644 --- a/internal/api/labels.go +++ b/internal/api/label.go @@ -2,13 +2,16 @@ package api import ( "fmt" + "io/ioutil" "net/http" "path" "strconv" + "time" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/repo" @@ -41,12 +44,12 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) { }) } -// POST /api/v1/labels/:slug/like +// POST /api/v1/labels/:uuid/like // // Parameters: -// slug: string Label slug name +// uuid: string Label UUID func LikeLabel(router *gin.RouterGroup, conf *config.Config) { - router.POST("/labels/:slug/like", func(c *gin.Context) { + router.POST("/labels/:uuid/like", func(c *gin.Context) { if Unauthorized(c, conf) { c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) return @@ -54,7 +57,7 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) { r := repo.New(conf.OriginalsPath(), conf.Db()) - label, err := r.FindLabelBySlug(c.Param("slug")) + label, err := r.FindLabelByUUID(c.Param("uuid")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -64,16 +67,22 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) { label.LabelFavorite = true conf.Db().Save(&label) + if label.LabelPriority < 0 { + event.Publish("count.labels", event.Data{ + "count": 1, + }) + } + c.JSON(http.StatusOK, http.Response{}) }) } -// DELETE /api/v1/labels/:slug/like +// DELETE /api/v1/labels/:uuid/like // // Parameters: -// slug: string Label slug name +// uuid: string Label UUID func DislikeLabel(router *gin.RouterGroup, conf *config.Config) { - router.DELETE("/labels/:slug/like", func(c *gin.Context) { + router.DELETE("/labels/:uuid/like", func(c *gin.Context) { if Unauthorized(c, conf) { c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) return @@ -81,7 +90,7 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) { r := repo.New(conf.OriginalsPath(), conf.Db()) - label, err := r.FindLabelBySlug(c.Param("slug")) + label, err := r.FindLabelByUUID(c.Param("uuid")) if err != nil { c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())}) @@ -91,20 +100,28 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) { label.LabelFavorite = false conf.Db().Save(&label) + if label.LabelPriority < 0 { + event.Publish("count.labels", event.Data{ + "count": -1, + }) + } + c.JSON(http.StatusOK, http.Response{}) }) } -// GET /api/v1/labels/:slug/thumbnail/:type +// GET /api/v1/labels/:uuid/thumbnail/:type // // Example: /api/v1/labels/cheetah/thumbnail/tile_500 // // Parameters: -// slug: string Label slug name +// uuid: string Label UUID // type: string Thumbnail type, see photoprism.ThumbnailTypes func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) { - router.GET("/labels/:slug/thumbnail/:type", func(c *gin.Context) { + router.GET("/labels/:uuid/thumbnail/:type", func(c *gin.Context) { typeName := c.Param("type") + labelUUID := c.Param("uuid") + start := time.Now() thumbType, ok := photoprism.ThumbnailTypes[typeName] @@ -116,11 +133,16 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) { r := repo.New(conf.OriginalsPath(), conf.Db()) - // log.Infof("Searching for label slug: %s", c.Param("slug")) + gc := conf.Cache() + cacheKey := fmt.Sprintf("label-thumbnail:%s:%s", labelUUID, typeName) - file, err := r.FindLabelThumbBySlug(c.Param("slug")) + if cacheData, ok := gc.Get(cacheKey); ok { + log.Debugf("%s cache hit [%s]", cacheKey, time.Since(start)) + c.Data(http.StatusOK, "image/jpeg", cacheData.([]byte)) + return + } - // log.Infof("Label thumb file: %#v", file) + file, err := r.FindLabelThumbByUUID(labelUUID) if err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": util.UcFirst(err.Error())}) @@ -140,16 +162,24 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) { } if thumbnail, err := photoprism.ThumbnailFromFile(fileName, file.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil { - if c.Query("download") != "" { - downloadFileName := file.DownloadFileName() + thumbData, err := ioutil.ReadFile(thumbnail) - c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName)) + if err != nil { + log.Errorf("could not read thumbnail: %s", err) + c.Data(http.StatusInternalServerError, "image/svg+xml", photoIconSvg) + return } - c.File(thumbnail) + gc.Set(cacheKey, thumbData, time.Hour * 4) + + log.Debugf("%s cached [%s]", cacheKey, time.Since(start)) + + c.Data(http.StatusOK, "image/jpeg", thumbData) } else { log.Errorf("could not create thumbnail: %s", err) - c.Data(http.StatusBadRequest, "image/svg+xml", photoIconSvg) + + c.Data(http.StatusInternalServerError, "image/svg+xml", photoIconSvg) + return } }) } diff --git a/internal/api/labels_test.go b/internal/api/label_test.go similarity index 100% rename from internal/api/labels_test.go rename to internal/api/label_test.go diff --git a/internal/config/config.go b/internal/config/config.go index a078d7196..4b9758514 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -595,7 +595,7 @@ func (c *Config) ClientConfig() ClientConfig { db.Table("labels"). Select("COUNT(*) AS labels"). - Where("label_priority >= 0 && deleted_at IS NULL"). + Where("(label_priority >= 0 || label_favorite = 1) && deleted_at IS NULL"). Take(&count) db.Table("albums"). diff --git a/internal/entity/album.go b/internal/entity/album.go index be0c587ce..fb7edcbed 100644 --- a/internal/entity/album.go +++ b/internal/entity/album.go @@ -7,7 +7,6 @@ import ( "github.com/gosimple/slug" "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/internal/util" - uuid "github.com/satori/go.uuid" ) // Photo album @@ -31,7 +30,7 @@ type Album struct { } func (m *Album) BeforeCreate(scope *gorm.Scope) error { - if err := scope.SetColumn("AlbumUUID", uuid.NewV4().String()); err != nil { + if err := scope.SetColumn("AlbumUUID", util.UUID()); err != nil { return err } diff --git a/internal/entity/event.go b/internal/entity/event.go index ab4d1e966..bec914bdf 100644 --- a/internal/entity/event.go +++ b/internal/entity/event.go @@ -4,8 +4,7 @@ import ( "time" "github.com/jinzhu/gorm" - - uuid "github.com/satori/go.uuid" + "github.com/photoprism/photoprism/internal/util" ) // Events @@ -31,5 +30,5 @@ func (Event) TableName() string { } func (e *Event) BeforeCreate(scope *gorm.Scope) error { - return scope.SetColumn("EventUUID", uuid.NewV4().String()) + return scope.SetColumn("EventUUID", util.UUID()) } diff --git a/internal/entity/file.go b/internal/entity/file.go index 5b3165ec1..81f1a7b69 100644 --- a/internal/entity/file.go +++ b/internal/entity/file.go @@ -6,7 +6,7 @@ import ( "github.com/gosimple/slug" "github.com/jinzhu/gorm" - uuid "github.com/satori/go.uuid" + "github.com/photoprism/photoprism/internal/util" ) // An image or sidecar file that belongs to a photo @@ -49,7 +49,7 @@ func FindFileByHash(db *gorm.DB, fileHash string) (File, error) { } func (m *File) BeforeCreate(scope *gorm.Scope) error { - return scope.SetColumn("FileUUID", uuid.NewV4().String()) + return scope.SetColumn("FileUUID", util.UUID()) } func (m *File) DownloadFileName() string { diff --git a/internal/entity/label.go b/internal/entity/label.go index 3ae1553c9..f6e469582 100644 --- a/internal/entity/label.go +++ b/internal/entity/label.go @@ -5,11 +5,13 @@ import ( "github.com/gosimple/slug" "github.com/jinzhu/gorm" + "github.com/photoprism/photoprism/internal/util" ) // Labels for photo, album and location categorization type Label struct { Model + LabelUUID string `gorm:"unique_index;"` LabelSlug string `gorm:"type:varchar(128);index;"` LabelName string `gorm:"type:varchar(128);"` LabelPriority int @@ -20,6 +22,14 @@ type Label struct { New bool `gorm:"-"` } +func (m *Label) BeforeCreate(scope *gorm.Scope) error { + if err := scope.SetColumn("LabelUUID", util.UUID()); err != nil { + return err + } + + return nil +} + func NewLabel(labelName string, labelPriority int) *Label { labelName = strings.TrimSpace(labelName) diff --git a/internal/entity/photo.go b/internal/entity/photo.go index 293763c64..87b1232b7 100644 --- a/internal/entity/photo.go +++ b/internal/entity/photo.go @@ -6,7 +6,6 @@ import ( "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/internal/util" - uuid "github.com/satori/go.uuid" ) // A photo can have multiple images and sidecar files @@ -55,7 +54,7 @@ type Photo struct { } func (m *Photo) BeforeCreate(scope *gorm.Scope) error { - if err := scope.SetColumn("PhotoUUID", uuid.NewV4().String()); err != nil { + if err := scope.SetColumn("PhotoUUID", util.UUID()); err != nil { return err } diff --git a/internal/entity/share.go b/internal/entity/share.go index f65668c00..b71bfd4cb 100644 --- a/internal/entity/share.go +++ b/internal/entity/share.go @@ -5,8 +5,6 @@ import ( "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/internal/util" - - uuid "github.com/satori/go.uuid" ) // Shared photos and/or albums @@ -14,6 +12,7 @@ type Share struct { ShareUUID string `gorm:"primary_key;auto_increment:false"` PhotoUUID string AlbumUUID string + LabelUUID string ShareViews uint ShareUrl string `gorm:"type:varchar(64);"` ShareToken string `gorm:"type:varchar(64);"` @@ -31,7 +30,7 @@ func (Share) TableName() string { } func (s *Share) BeforeCreate(scope *gorm.Scope) error { - if err := scope.SetColumn("ShareUUID", uuid.NewV4().String()); err != nil { + if err := scope.SetColumn("ShareUUID", util.UUID()); err != nil { return err } diff --git a/internal/repo/albums.go b/internal/repo/album.go similarity index 100% rename from internal/repo/albums.go rename to internal/repo/album.go diff --git a/internal/repo/files.go b/internal/repo/file.go similarity index 100% rename from internal/repo/files.go rename to internal/repo/file.go diff --git a/internal/repo/labels.go b/internal/repo/label.go similarity index 73% rename from internal/repo/labels.go rename to internal/repo/label.go index 68e26e27f..72cf50973 100644 --- a/internal/repo/labels.go +++ b/internal/repo/label.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/gosimple/slug" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/util" @@ -17,6 +18,7 @@ type LabelResult struct { CreatedAt time.Time UpdatedAt time.Time DeletedAt time.Time + LabelUUID string LabelSlug string LabelName string LabelPriority int @@ -35,6 +37,15 @@ func (s *Repo) FindLabelBySlug(labelSlug string) (label entity.Label, err error) return label, nil } +// FindLabelByUUID returns a Label based on the label UUID. +func (s *Repo) FindLabelByUUID(labelUUID string) (label entity.Label, err error) { + if err := s.db.Where("label_uuid = ?", labelUUID).First(&label).Error; err != nil { + return label, err + } + + return label, nil +} + // FindLabelThumbBySlug returns a label preview file based on the slug name. func (s *Repo) FindLabelThumbBySlug(labelSlug string) (file entity.File, err error) { // s.db.LogMode(true) @@ -50,6 +61,21 @@ func (s *Repo) FindLabelThumbBySlug(labelSlug string) (file entity.File, err err return file, nil } +// FindLabelThumbByUUID returns a label preview file based on the label UUID. +func (s *Repo) FindLabelThumbByUUID(labelUUID string) (file entity.File, err error) { + // s.db.LogMode(true) + + if err := s.db.Where("files.file_primary AND files.deleted_at IS NULL"). + Joins("JOIN labels ON labels.label_uuid = ?", labelUUID). + Joins("JOIN photos_labels ON photos_labels.label_id = labels.id AND photos_labels.photo_id = files.photo_id"). + Order("photos_labels.label_uncertainty ASC"). + First(&file).Error; err != nil { + return file, err + } + + return file, nil +} + // Labels searches labels based on their name. func (s *Repo) Labels(f form.LabelSearch) (results []LabelResult, err error) { if err := f.ParseQueryString(); err != nil { @@ -63,8 +89,7 @@ func (s *Repo) Labels(f form.LabelSearch) (results []LabelResult, err error) { // q.LogMode(true) q = q.Table("labels"). - Select(`labels.*, COUNT(photos_labels.label_id) AS label_count`). - Joins("JOIN photos_labels ON photos_labels.label_id = labels.id"). + Select(`labels.*`). Where("labels.deleted_at IS NULL"). Group("labels.id") @@ -73,9 +98,10 @@ func (s *Repo) Labels(f form.LabelSearch) (results []LabelResult, err error) { var categories []entity.Category var label entity.Label + slugString := slug.Make(f.Query) likeString := "%" + strings.ToLower(f.Query) + "%" - if result := s.db.First(&label, "LOWER(label_name) LIKE LOWER(?)", f.Query); result.Error != nil { + if result := s.db.First(&label, "label_slug = ?", slugString); result.Error != nil { log.Infof("search: label \"%s\" not found", f.Query) q = q.Where("LOWER(labels.label_name) LIKE ?", likeString) @@ -90,7 +116,7 @@ func (s *Repo) Labels(f form.LabelSearch) (results []LabelResult, err error) { log.Infof("search: label \"%s\" includes %d categories", label.LabelName, len(labelIds)) - q = q.Where("labels.id IN (?) OR LOWER(labels.label_name) LIKE ?", labelIds, likeString) + q = q.Where("labels.id IN (?)", labelIds) } } @@ -101,7 +127,7 @@ func (s *Repo) Labels(f form.LabelSearch) (results []LabelResult, err error) { if f.Priority != 0 { q = q.Where("labels.label_priority > ?", f.Priority) } else { - q = q.Where("labels.label_priority >= 0") + q = q.Where("labels.label_priority >= 0 OR labels.label_favorite = 1") } switch f.Order { diff --git a/internal/repo/photos.go b/internal/repo/photo.go similarity index 100% rename from internal/repo/photos.go rename to internal/repo/photo.go diff --git a/internal/repo/photos_test.go b/internal/repo/photo_test.go similarity index 100% rename from internal/repo/photos_test.go rename to internal/repo/photo_test.go diff --git a/internal/util/token.go b/internal/util/token.go index d459d1c59..48ab334cf 100644 --- a/internal/util/token.go +++ b/internal/util/token.go @@ -3,6 +3,8 @@ package util import ( "crypto/rand" "fmt" + + uuid "github.com/satori/go.uuid" ) func RandomToken(size int) string { @@ -14,3 +16,7 @@ func RandomToken(size int) string { return fmt.Sprintf("%x", b) } + +func UUID() string { + return uuid.NewV4().String() +}