mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Labels: Add cache, fix bugs & improve SQL queries
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ class Label extends Abstract {
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.LabelSlug;
|
||||
return this.LabelUUID;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<v-text-field class="pt-3 pr-3"
|
||||
single-line
|
||||
:label="labels.search"
|
||||
browser-autocomplete="off"
|
||||
prepend-inner-icon="search"
|
||||
clearable
|
||||
color="secondary-dark"
|
||||
@@ -18,18 +19,25 @@
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn icon @click.stop="refresh" class="hidden-xs-only">
|
||||
<v-icon>refresh</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click.prevent="create">
|
||||
<v-icon>add</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</v-form>
|
||||
|
||||
<v-container fluid class="pa-2">
|
||||
<v-container fluid class="pa-4" v-if="loading">
|
||||
<v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear>
|
||||
</v-container>
|
||||
<v-container fluid class="pa-0" v-else>
|
||||
<p-scroll-top></p-scroll-top>
|
||||
|
||||
<p-album-clipboard :refresh="refresh" :selection="selection"></p-album-clipboard>
|
||||
|
||||
<v-container grid-list-xs fluid class="pa-0 p-albums p-albums-details">
|
||||
<v-container grid-list-xs fluid class="pa-2 p-albums p-albums-details">
|
||||
<v-card v-if="results.length === 0" class="p-albums-empty secondary-light lighten-1" flat>
|
||||
<v-card-title primary-title>
|
||||
<div>
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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 @@
|
||||
></v-text-field>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn icon @click.stop="refresh" class="hidden-xs-only">
|
||||
<v-icon>refresh</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</v-form>
|
||||
|
||||
<v-container fluid class="pa-2">
|
||||
<v-container fluid class="pa-4" v-if="loading">
|
||||
<v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear>
|
||||
</v-container>
|
||||
<v-container fluid class="pa-0" v-else>
|
||||
<p-scroll-top></p-scroll-top>
|
||||
|
||||
<v-container grid-list-xs fluid class="pa-0 p-labels p-labels-details">
|
||||
<v-card v-if="results.length === 0" class="p-labels-empty" flat>
|
||||
<v-container grid-list-xs fluid class="pa-2 p-labels p-labels-details">
|
||||
<v-card v-if="results.length === 0" class="p-labels-empty secondary-light lighten-1" flat>
|
||||
<v-card-title primary-title>
|
||||
<div>
|
||||
<h3 class="title mb-3"><translate>No labels matched your search</translate></h3>
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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").
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user