People: Refactor album, subject, and label previews #22

This commit is contained in:
Michael Mayer
2021-08-30 18:58:27 +02:00
parent 847f41d98c
commit bc3036599b
37 changed files with 905 additions and 496 deletions

View File

@@ -1900,9 +1900,9 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
},
"node_modules/@types/node": {
"version": "16.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.5.tgz",
"integrity": "sha512-E7SpxDXoHEpmZ9C1gSqwadhE6zPRtf3g0gJy9Y51DsImnR5TcDs3QEiV/3Q7zOM8LWaZp5Gph71NK6ElVMG1IQ=="
"version": "16.7.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.6.tgz",
"integrity": "sha512-VESVNFoa/ahYA62xnLBjo5ur6gPsgEE5cNRy8SrdnkZ2nwJSW0kJ4ufbFr2zuU9ALtHM8juY53VcRoTA7htXSg=="
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
@@ -2823,9 +2823,9 @@
}
},
"node_modules/babel-loader/node_modules/find-cache-dir": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz",
"integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
"integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
"dependencies": {
"commondir": "^1.0.1",
"make-dir": "^3.0.2",
@@ -3846,9 +3846,9 @@
}
},
"node_modules/core-js": {
"version": "3.16.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.3.tgz",
"integrity": "sha512-lM3GftxzHNtPNUJg0v4pC2RC6puwMd6VZA7vXUczi+SKmCWSf4JwO89VJGMqbzmB7jlK7B5hr3S64PqwFL49cA==",
"version": "3.16.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.4.tgz",
"integrity": "sha512-Tq4GVE6XCjE+hcyW6hPy0ofN3hwtLudz5ZRdrlCnsnD/xkm/PWQRudzYHiKgZKUcefV6Q57fhDHjZHJP5dpfSg==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
@@ -3856,9 +3856,9 @@
}
},
"node_modules/core-js-compat": {
"version": "3.16.3",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.16.3.tgz",
"integrity": "sha512-A/OtSfSJQKLAFRVd4V0m6Sep9lPdjD8bpN8v3tCCGwE0Tmh0hOiVDm9tw6mXmWOKOSZIyr3EkywPo84cJjGvIQ==",
"version": "3.16.4",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.16.4.tgz",
"integrity": "sha512-IzCSomxRdahCYb6G3HiN6pl3JCiM0NMunRcNa1pIeC7g17Vd6Ue3AT9anQiENPIm/svThUVer1pIbLMDERIsFw==",
"dependencies": {
"browserslist": "^4.16.8",
"semver": "7.0.0"
@@ -17191,9 +17191,9 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
},
"@types/node": {
"version": "16.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.5.tgz",
"integrity": "sha512-E7SpxDXoHEpmZ9C1gSqwadhE6zPRtf3g0gJy9Y51DsImnR5TcDs3QEiV/3Q7zOM8LWaZp5Gph71NK6ElVMG1IQ=="
"version": "16.7.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.6.tgz",
"integrity": "sha512-VESVNFoa/ahYA62xnLBjo5ur6gPsgEE5cNRy8SrdnkZ2nwJSW0kJ4ufbFr2zuU9ALtHM8juY53VcRoTA7htXSg=="
},
"@types/parse-json": {
"version": "4.0.0",
@@ -17916,9 +17916,9 @@
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
},
"find-cache-dir": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz",
"integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
"integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
"requires": {
"commondir": "^1.0.1",
"make-dir": "^3.0.2",
@@ -18737,14 +18737,14 @@
"optional": true
},
"core-js": {
"version": "3.16.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.3.tgz",
"integrity": "sha512-lM3GftxzHNtPNUJg0v4pC2RC6puwMd6VZA7vXUczi+SKmCWSf4JwO89VJGMqbzmB7jlK7B5hr3S64PqwFL49cA=="
"version": "3.16.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.4.tgz",
"integrity": "sha512-Tq4GVE6XCjE+hcyW6hPy0ofN3hwtLudz5ZRdrlCnsnD/xkm/PWQRudzYHiKgZKUcefV6Q57fhDHjZHJP5dpfSg=="
},
"core-js-compat": {
"version": "3.16.3",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.16.3.tgz",
"integrity": "sha512-A/OtSfSJQKLAFRVd4V0m6Sep9lPdjD8bpN8v3tCCGwE0Tmh0hOiVDm9tw6mXmWOKOSZIyr3EkywPo84cJjGvIQ==",
"version": "3.16.4",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.16.4.tgz",
"integrity": "sha512-IzCSomxRdahCYb6G3HiN6pl3JCiM0NMunRcNa1pIeC7g17Vd6Ue3AT9anQiENPIm/svThUVer1pIbLMDERIsFw==",
"requires": {
"browserslist": "^4.16.8",
"semver": "7.0.0"

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -153,7 +153,7 @@ msgstr ""
msgid "After two weeks"
msgstr ""
#: src/model/album.js:182
#: src/model/album.js:189
msgid "Album"
msgstr ""
@@ -271,7 +271,7 @@ msgstr ""
#: src/component/photo/cards.vue:26
#: src/component/photo/clipboard.vue:100
#: src/dialog/photo/details.vue:120
#: src/dialog/photo/people.vue:149
#: src/dialog/photo/people.vue:151
#: src/share/photo/cards.vue:26
msgid "Approve"
msgstr ""
@@ -342,7 +342,7 @@ msgstr ""
msgid "Blue"
msgstr ""
#: src/options/options.js:151
#: src/options/options.js:156
msgid "Brazilian Portuguese"
msgstr ""
@@ -516,7 +516,7 @@ msgstr ""
msgid "Convert to JPEG"
msgstr ""
#: src/pages/library/index.vue:162
#: src/pages/library/index.vue:166
msgid "Converting"
msgstr ""
@@ -574,7 +574,7 @@ msgstr ""
msgid "Created"
msgstr ""
#: src/pages/library/index.vue:169
#: src/pages/library/index.vue:173
msgid "Creating thumbnails for"
msgstr ""
@@ -1215,7 +1215,7 @@ msgid "Kurdish"
msgstr ""
#: src/dialog/photo/labels.vue:17
#: src/model/label.js:114
#: src/model/label.js:120
msgid "Label"
msgstr ""
@@ -1464,6 +1464,7 @@ msgstr ""
#: src/dialog/photo/files.vue:67
#: src/dialog/photo/files.vue:30
#: src/dialog/photo/info.vue:31
#: src/dialog/photo/people.vue:19
#: src/pages/about/feedback.vue:144
#: src/pages/login.vue:73
#: src/share/photo/cards.vue:30
@@ -1721,7 +1722,7 @@ msgstr ""
msgid "Permanently remove files to free up storage."
msgstr ""
#: src/model/photo.js:875
#: src/model/photo.js:900
msgid "Photo"
msgstr ""
@@ -1794,7 +1795,7 @@ msgstr ""
msgid "Portrait"
msgstr ""
#: src/options/options.js:156
#: src/options/options.js:151
msgid "Português de Portugal"
msgstr ""
@@ -1904,7 +1905,7 @@ msgstr ""
msgid "Red"
msgstr ""
#: src/dialog/photo/people.vue:121
#: src/dialog/photo/people.vue:122
msgid "Reject"
msgstr ""
@@ -1928,14 +1929,14 @@ msgstr ""
msgid "Remote Sync"
msgstr ""
#: src/component/photo/clipboard.vue:260
msgid "Remove"
msgstr ""
#: src/component/photo/clipboard.vue:93
msgid "remove failed: unknown album"
msgstr ""
#: src/component/photo/clipboard.vue:260
msgid "Remove from album"
msgstr ""
#: src/pages/library/import.vue:130
msgid "Remove imported files to save storage. Unsupported file types will never be deleted, they remain in their current location."
msgstr ""
@@ -2235,6 +2236,7 @@ msgid "Style"
msgstr ""
#: src/dialog/photo/details.vue:482
#: src/model/subject.js:124
msgid "Subject"
msgstr ""
@@ -2264,7 +2266,7 @@ msgid "Teal"
msgstr ""
#: src/dialog/photo/details.vue:26
#: src/dialog/photo/people.vue:14
#: src/dialog/photo/people.vue:22
msgid "Text too long"
msgstr ""
@@ -2372,7 +2374,7 @@ msgstr ""
#: src/dialog/photo/details.vue:16
#: src/dialog/photo/info.vue:21
#: src/model/album.js:139
#: src/model/album.js:146
#: src/model/photo.js:526
#: src/model/photo.js:543
#: src/model/photo.js:566
@@ -2407,6 +2409,10 @@ msgid "Updated"
msgstr ""
#: src/pages/library/index.vue:153
msgid "Updating faces"
msgstr ""
#: src/pages/library/index.vue:157
msgid "Updating index"
msgstr ""
@@ -2414,6 +2420,10 @@ msgstr ""
msgid "Updating moments"
msgstr ""
#: src/pages/library/index.vue:155
msgid "Updating previews"
msgstr ""
#: src/pages/library/index.vue:149
msgid "Updating stacks"
msgstr ""

View File

@@ -38,9 +38,10 @@ export class Album extends RestModel {
getDefaults() {
return {
UID: "",
Cover: "",
Parent: "",
Folder: "",
ParentUID: "",
Thumb: "",
ThumbSrc: "",
Path: "",
Slug: "",
Type: "",
Title: "",
@@ -91,7 +92,13 @@ export class Album extends RestModel {
}
thumbnailUrl(size) {
return `${config.contentUri}/albums/${this.getId()}/t/${config.previewToken()}/${size}`;
if (this.Thumb) {
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
} else if (this.UID) {
return `${config.contentUri}/albums/${this.UID}/t/${config.previewToken()}/${size}`;
} else {
return `${config.contentUri}/svg/album`;
}
}
dayString() {

View File

@@ -75,7 +75,13 @@ export class Label extends RestModel {
}
thumbnailUrl(size) {
return `${config.contentUri}/labels/${this.getId()}/t/${config.previewToken()}/${size}`;
if (this.Thumb) {
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
} else if (this.UID) {
return `${config.contentUri}/labels/${this.UID}/t/${config.previewToken()}/${size}`;
} else {
return `${config.contentUri}/svg/label`;
}
}
getDateString() {

View File

@@ -0,0 +1,128 @@
/*
Copyright (c) 2018 - 2021 Michael Mayer <hello@photoprism.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
PhotoPrism® is a registered trademark of Michael Mayer. You may use it as required
to describe our software, run your own server, for educational purposes, but not for
offering commercial goods, products, or services without prior written permission.
In other words, please ask.
Feel free to send an e-mail to hello@photoprism.org if you have questions,
want to support our work, or just want to say hello.
Additional information can be found in our Developer Guide:
https://docs.photoprism.org/developer-guide/
*/
import RestModel from "model/rest";
import Api from "common/api";
import { DateTime } from "luxon";
import { config } from "../session";
import { $gettext } from "common/vm";
export class Subject extends RestModel {
getDefaults() {
return {
UID: "",
Thumb: "",
PreviewSrc: "",
Type: "",
Src: "",
Slug: "",
Name: "",
Bio: "",
Notes: "",
Favorite: false,
Private: false,
Excluded: false,
FileCount: 0,
Metadata: {},
CreatedAt: "",
UpdatedAt: "",
DeletedAt: "",
};
}
route(view) {
return { name: view, query: { q: "subject:" + this.UID } };
}
classes(selected) {
let classes = ["is-subject", "uid-" + this.UID];
if (this.Favorite) classes.push("is-favorite");
if (this.Private) classes.push("is-private");
if (this.Excluded) classes.push("is-excluded");
if (selected) classes.push("is-selected");
return classes;
}
getEntityName() {
return this.Slug;
}
getTitle() {
return this.Name;
}
thumbnailUrl(size) {
if (this.Thumb) {
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
} else {
return `${config.contentUri}/svg/portrait`;
}
}
getDateString() {
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
}
toggleLike() {
this.Favorite = !this.Favorite;
if (this.Favorite) {
return Api.post(this.getEntityResource() + "/like");
} else {
return Api.delete(this.getEntityResource() + "/like");
}
}
like() {
this.Favorite = true;
return Api.post(this.getEntityResource() + "/like");
}
unlike() {
this.Favorite = false;
return Api.delete(this.getEntityResource() + "/like");
}
static batchSize() {
return 24;
}
static getCollectionResource() {
return "subjects";
}
static getModelName() {
return $gettext("Subject");
}
}
export default Subject;

View File

@@ -240,6 +240,8 @@ export default {
this.action = this.$gettext("Updating moments");
} else if (data.step === "faces") {
this.action = this.$gettext("Updating faces");
} else if (data.step === "previews") {
this.action = this.$gettext("Updating previews");
} else {
this.action = this.$gettext("Updating index");
}

View File

@@ -33,31 +33,37 @@ describe("model/album", () => {
});
it("should get album entity name", () => {
const values = { id: 5, Title: "Christmas 2019", Slug: "christmas-2019" };
const values = { ID: 5, Title: "Christmas 2019", Slug: "christmas-2019" };
const album = new Album(values);
const result = album.getEntityName();
assert.equal(result, "christmas-2019");
});
it("should get album id", () => {
const values = { id: 5, Title: "Christmas 2019", Slug: "christmas-2019", UID: 66 };
const values = { ID: 5, Title: "Christmas 2019", Slug: "christmas-2019", UID: 66 };
const album = new Album(values);
const result = album.getId();
assert.equal(result, "66");
});
it("should get album title", () => {
const values = { id: 5, Title: "Christmas 2019", Slug: "christmas-2019" };
const values = { ID: 5, Title: "Christmas 2019", Slug: "christmas-2019" };
const album = new Album(values);
const result = album.getTitle();
assert.equal(result, "Christmas 2019");
});
it("should get thumbnail url", () => {
const values = { id: 5, Title: "Christmas 2019", Slug: "christmas-2019", UID: 66 };
const values = {
ID: 5,
Thumb: "d6b24d688564f7ddc7b245a414f003a8d8ff5a67",
Title: "Christmas 2019",
Slug: "christmas-2019",
UID: 66,
};
const album = new Album(values);
const result = album.thumbnailUrl("xyz");
assert.equal(result, "/api/v1/albums/66/t/public/xyz");
assert.equal(result, "/api/v1/t/d6b24d688564f7ddc7b245a414f003a8d8ff5a67/public/xyz");
});
it("should get created date string", () => {

View File

@@ -49,10 +49,16 @@ describe("model/label", () => {
});
it("should get thumbnail url", () => {
const values = { ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat" };
const values = {
ID: 5,
UID: "ABC123",
Thumb: "c6b24d688564f7ddc7b245a414f003a8d8ff5a67",
Name: "Black Cat",
Slug: "black-cat",
};
const label = new Label(values);
const result = label.thumbnailUrl("xyz");
assert.equal(result, "/api/v1/labels/ABC123/t/public/xyz");
assert.equal(result, "/api/v1/t/c6b24d688564f7ddc7b245a414f003a8d8ff5a67/public/xyz");
});
it("should get date string", () => {

View File

@@ -16,6 +16,8 @@ import (
"github.com/photoprism/photoprism/internal/query"
)
// BatchPhotosArchive moves multiple photos to the archive.
//
// POST /api/v1/batch/photos/archive
func BatchPhotosArchive(router *gin.RouterGroup) {
router.POST("/batch/photos/archive", func(c *gin.Context) {
@@ -64,7 +66,7 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
}
logError("photos", entity.UpdatePhotoCounts())
logError("photos", query.ToggleMonthAlbums())
logError("photos", query.UpdatePreviews())
UpdateClientConfig()
@@ -74,6 +76,8 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
})
}
// BatchPhotosRestore restores multiple photos from the archive.
//
// POST /api/v1/batch/photos/restore
func BatchPhotosRestore(router *gin.RouterGroup) {
router.POST("/batch/photos/restore", func(c *gin.Context) {
@@ -121,7 +125,7 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
}
logError("photos", entity.UpdatePhotoCounts())
logError("photos", query.ToggleMonthAlbums())
logError("photos", query.UpdatePreviews())
UpdateClientConfig()
@@ -131,6 +135,8 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
})
}
// BatchPhotosApprove approves multiple photos that are currently under review.
//
// POST /api/v1/batch/photos/approve
func BatchPhotosApprove(router *gin.RouterGroup) {
router.POST("batch/photos/approve", func(c *gin.Context) {
@@ -181,6 +187,8 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
})
}
// BatchAlbumsDelete permanently deletes multiple albums.
//
// POST /api/v1/batch/albums/delete
func BatchAlbumsDelete(router *gin.RouterGroup) {
router.POST("/batch/albums/delete", func(c *gin.Context) {
@@ -216,6 +224,8 @@ func BatchAlbumsDelete(router *gin.RouterGroup) {
})
}
// BatchPhotosPrivate flags multiple photos as private.
//
// POST /api/v1/batch/photos/private
func BatchPhotosPrivate(router *gin.RouterGroup) {
router.POST("/batch/photos/private", func(c *gin.Context) {
@@ -248,7 +258,6 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
}
logError("photos", entity.UpdatePhotoCounts())
logError("photos", query.ToggleMonthAlbums())
if photos, err := query.PhotoSelection(f); err == nil {
for _, p := range photos {
@@ -266,6 +275,8 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
})
}
// BatchLabelsDelete deletes multiple labels.
//
// POST /api/v1/batch/labels/delete
func BatchLabelsDelete(router *gin.RouterGroup) {
router.POST("/batch/labels/delete", func(c *gin.Context) {
@@ -310,6 +321,8 @@ func BatchLabelsDelete(router *gin.RouterGroup) {
})
}
// BatchPhotosDelete permanently deletes multiple photos from the archive.
//
// POST /api/v1/batch/photos/delete
func BatchPhotosDelete(router *gin.RouterGroup) {
router.POST("/batch/photos/delete", func(c *gin.Context) {

View File

@@ -4,6 +4,8 @@ import (
"fmt"
"strconv"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
)
@@ -44,6 +46,10 @@ func RemoveFromFolderCache(rootName string) {
cache.Delete(cacheKey)
if err := query.UpdateAlbumFolderPreviews(); err != nil {
log.Errorf("failed updating folder previews: %s", err)
}
log.Debugf("removed %s from cache", cacheKey)
}
@@ -58,11 +64,19 @@ func RemoveFromAlbumCoverCache(uid string) {
log.Debugf("removed %s from cache", cacheKey)
}
if err := query.UpdateAlbumPreviews(); err != nil {
log.Errorf("failed updating album previews: %s", err)
}
}
// FlushCoverCache clears the complete cover cache.
func FlushCoverCache() {
service.CoverCache().Flush()
if err := query.UpdatePreviews(); err != nil {
log.Errorf("failed updating preview images: %s", err)
}
log.Debugf("albums: flushed cover cache")
}

View File

@@ -7,6 +7,8 @@ import (
"strings"
"time"
"github.com/photoprism/photoprism/internal/query"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
@@ -107,6 +109,11 @@ func StartImport(router *gin.RouterGroup) {
UpdateClientConfig()
// Update album, label, and subject preview images.
if err := query.UpdatePreviews(); err != nil {
log.Errorf("import: %s (update previews)", err)
}
c.JSON(http.StatusOK, i18n.Response{Code: http.StatusOK, Msg: msg})
})
}

View File

@@ -90,14 +90,14 @@ func UpdatePhoto(router *gin.RouterGroup) {
if err := c.BindJSON(&f); err != nil {
Abort(c, http.StatusBadRequest, i18n.ErrBadRequest)
return
} else if f.PhotoPrivate {
FlushCoverCache()
}
// 3) Save model with values from form
if err := entity.SavePhotoForm(m, f); err != nil {
Abort(c, http.StatusInternalServerError, i18n.ErrSaveFailed)
return
} else if f.PhotoPrivate {
FlushCoverCache()
}
PublishPhotoEvent(EntityUpdated, uid, c)

View File

@@ -28,6 +28,9 @@ var albumIconSvg = folderIconSvg
var labelIconSvg = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/><path d="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12l-4.37-6.16z"/></svg>`)
var portraitIconSvg = []byte(`
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 12c1.65 0 3-1.35 3-3s-1.35-3-3-3-3 1.35-3 3 1.35 3 3 3zm0-4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm6 8.58c0-2.5-3.97-3.58-6-3.58s-6 1.08-6 3.58V18h12v-1.42zM8.48 16c.74-.51 2.23-1 3.52-1s2.78.49 3.52 1H8.48zM19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z"/></svg>`)
var brokenIconSvg = []byte(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0zm0 0h24v24H0zm21 19c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2"/>
@@ -60,6 +63,10 @@ func GetSvg(router *gin.RouterGroup) {
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
})
router.GET("/svg/portrait", func(c *gin.Context) {
c.Data(http.StatusOK, "image/svg+xml", portraitIconSvg)
})
router.GET("/svg/folder", func(c *gin.Context) {
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
})

View File

@@ -29,10 +29,11 @@ type Albums []Album
type Album struct {
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
AlbumUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
CoverUID string `gorm:"type:VARBINARY(42);" json:"CoverUID" yaml:"CoverUID,omitempty"`
FolderUID string `gorm:"type:VARBINARY(42);index;" json:"FolderUID" yaml:"FolderUID,omitempty"`
ParentUID string `gorm:"type:VARBINARY(42);default:''" json:"ParentUID,omitempty" yaml:"ParentUID,omitempty"`
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
AlbumSlug string `gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"Slug"`
AlbumPath string `gorm:"type:VARBINARY(500);index;" json:"Path" yaml:"-"`
AlbumPath string `gorm:"type:VARBINARY(500);index;" json:"Path,omitempty" yaml:"Path,omitempty"`
AlbumType string `gorm:"type:VARBINARY(8);default:'album';" json:"Type" yaml:"Type,omitempty"`
AlbumTitle string `gorm:"type:VARCHAR(255);index;" json:"Title" yaml:"Title"`
AlbumLocation string `gorm:"type:VARCHAR(255);" json:"Location" yaml:"Location,omitempty"`
@@ -55,6 +56,11 @@ type Album struct {
Photos PhotoAlbums `gorm:"foreignkey:AlbumUID;association_foreignkey:AlbumUID" json:"-" yaml:"Photos,omitempty"`
}
// TableName returns the entity database table name.
func (Album) TableName() string {
return "albums"
}
// AddPhotoToAlbums adds a photo UID to multiple albums and automatically creates them if needed.
func AddPhotoToAlbums(photo string, albums []string) (err error) {
if photo == "" || len(albums) == 0 {

View File

@@ -25,9 +25,7 @@ func (m AlbumMap) Pointer(name string) *Album {
var AlbumFixtures = AlbumMap{
"christmas2030": {
ID: 1000000,
CoverUID: "",
AlbumUID: "at9lxuqxpogaaba7",
FolderUID: "",
AlbumSlug: "christmas-2030",
AlbumPath: "",
AlbumType: AlbumDefault,
@@ -52,9 +50,7 @@ var AlbumFixtures = AlbumMap{
},
"holiday-2030": {
ID: 1000001,
CoverUID: "",
AlbumUID: "at9lxuqxpogaaba8",
FolderUID: "",
AlbumSlug: "holiday-2030",
AlbumPath: "",
AlbumType: AlbumDefault,
@@ -79,9 +75,7 @@ var AlbumFixtures = AlbumMap{
},
"berlin-2019": {
ID: 1000002,
CoverUID: "",
AlbumUID: "at9lxuqxpogaaba9",
FolderUID: "",
AlbumSlug: "berlin-2019",
AlbumPath: "",
AlbumType: AlbumDefault,
@@ -106,9 +100,7 @@ var AlbumFixtures = AlbumMap{
},
"april-1990": {
ID: 1000003,
CoverUID: "",
AlbumUID: "at1lxuqipogaaba1",
FolderUID: "",
AlbumSlug: "april-1990",
AlbumPath: "1990/04",
AlbumType: AlbumFolder,
@@ -133,9 +125,7 @@ var AlbumFixtures = AlbumMap{
},
"import": {
ID: 1000004,
CoverUID: "",
AlbumUID: "at6axuzitogaaiax",
FolderUID: "",
AlbumSlug: "import",
AlbumPath: "",
AlbumType: AlbumDefault,
@@ -160,9 +150,7 @@ var AlbumFixtures = AlbumMap{
},
"emptyMoment": {
ID: 1000005,
CoverUID: "",
AlbumUID: "at7axuzitogaaiax",
FolderUID: "",
AlbumSlug: "empty-moment",
AlbumPath: "",
AlbumType: AlbumMoment,
@@ -187,9 +175,7 @@ var AlbumFixtures = AlbumMap{
},
"2016-04": {
ID: 1000006,
CoverUID: "",
AlbumUID: "at1lxuqipogaabj8",
FolderUID: "",
AlbumSlug: "2016-04",
AlbumPath: "2016/04",
AlbumType: AlbumFolder,

View File

@@ -20,6 +20,8 @@ type Labels []Label
type Label struct {
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
LabelUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
LabelSlug string `gorm:"type:VARBINARY(255);unique_index;" json:"Slug" yaml:"-"`
CustomSlug string `gorm:"type:VARBINARY(255);index;" json:"CustomSlug" yaml:"-"`
LabelName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name"`
@@ -35,6 +37,11 @@ type Label struct {
New bool `gorm:"-" json:"-" yaml:"-"`
}
// TableName returns the entity database table name.
func (Label) TableName() string {
return "labels"
}
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Label) BeforeCreate(scope *gorm.Scope) error {
if rnd.IsUID(m.LabelUID, 'l') {

View File

@@ -102,6 +102,11 @@ type Photo struct {
DeletedAt *time.Time `sql:"index" yaml:"DeletedAt,omitempty"`
}
// TableName returns the entity database table name.
func (Photo) TableName() string {
return "photos"
}
// NewPhoto creates a photo entity.
func NewPhoto(stackable bool) Photo {
m := Photo{

View File

@@ -41,11 +41,10 @@ func LabelCounts() LabelPhotoCounts {
return result
}
// UpdatePhotoCounts updates photos count in related tables as needed.
func UpdatePhotoCounts() error {
// log.Info("index: updating photo counts")
if err := Db().Table("places").
// UpdatePhotoCounts updates static photos counts and visibilities.
func UpdatePhotoCounts() (err error) {
// Update places.
if err = Db().Table("places").
UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(*) FROM photos p "+
"WHERE places.id = p.place_id "+
"AND p.photo_quality >= 0 "+
@@ -54,39 +53,20 @@ func UpdatePhotoCounts() error {
return err
}
/* See internal/entity/views.go
CREATE OR REPLACE VIEW label_counts AS
SELECT label_id, SUM(photo_count) AS photo_count FROM (
(SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
JOIN photos_labels pl ON pl.label_id = l.id
JOIN photos ph ON pl.photo_id = ph.id
WHERE pl.uncertainty < 100
AND ph.photo_quality >= 0
AND ph.photo_private = 0
AND ph.deleted_at IS NULL GROUP BY l.id)
UNION ALL
(SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
JOIN categories c ON c.category_id = l.id
JOIN photos_labels pl ON pl.label_id = c.label_id
JOIN photos ph ON pl.photo_id = ph.id
WHERE pl.uncertainty < 100
AND ph.photo_quality >= 0
AND ph.photo_private = 0
AND ph.deleted_at IS NULL GROUP BY l.id)) counts GROUP BY label_id
*/
/* TODO: Requires view support
if err := Db().
Table("labels").
UpdateColumn("photo_count",
gorm.Expr("(SELECT photo_count FROM label_counts WHERE label_id = labels.id)")).Error; err != nil {
log.Warn(err)
} */
// Update subjects.
if err = Db().Table(Subject{}.TableName()).
UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(*) FROM files f "+
fmt.Sprintf(
"JOIN %s m ON f.id = m.file_id AND m.subject_uid = %s.subject_uid ",
Marker{}.TableName(),
Subject{}.TableName())+
" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL)")).Error; err != nil {
return err
}
// Update labels.
if IsDialect(MySQL) {
if err := Db().
if err = Db().
Table("labels").
UpdateColumn("photo_count",
gorm.Expr(`(SELECT photo_count FROM (
@@ -111,7 +91,7 @@ func UpdatePhotoCounts() error {
return err
}
} else if IsDialect(SQLite) {
if err := Db().
if err = Db().
Table("labels").
UpdateColumn("photo_count",
gorm.Expr(`(SELECT photo_count FROM (SELECT label_id, SUM(photo_count) AS photo_count FROM (
@@ -137,5 +117,22 @@ func UpdatePhotoCounts() error {
return fmt.Errorf("unknown sql dialect %s", DbDialect())
}
// Update calendar album visibility.
switch DbDialect() {
default:
if err = UnscopedDb().Exec(`UPDATE albums SET deleted_at = ? WHERE album_type=? AND id NOT IN (
SELECT a.id FROM albums a JOIN photos p ON a.album_month = p.photo_month AND a.album_year = p.photo_year
AND p.deleted_at IS NULL AND p.photo_quality > -1 AND p.photo_private = 0 WHERE album_type=?)`,
TimeStamp(), AlbumMonth, AlbumMonth).Error; err != nil {
return err
}
if err = UnscopedDb().Exec(`UPDATE albums SET deleted_at = NULL WHERE album_type=? AND id IN (
SELECT a.id FROM albums a JOIN photos p ON a.album_month = p.photo_month AND a.album_year = p.photo_year
AND p.deleted_at IS NULL AND p.photo_quality > -1 AND p.photo_private = 0 WHERE album_type=?)`,
AlbumMonth, AlbumMonth).Error; err != nil {
return err
}
}
return nil
}

View File

@@ -23,22 +23,23 @@ type Subjects []Subject
// Subject represents a named photo subject, typically a person.
type Subject struct {
SubjectUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
SubjectType string `gorm:"type:VARBINARY(8);" json:"Type" yaml:"Type"`
SubjectSrc string `gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src"`
SubjectSlug string `gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"-"`
SubjectName string `gorm:"type:VARCHAR(255);unique_index;" json:"Name" yaml:"Name"`
SubjectDescription string `gorm:"type:TEXT;" json:"Description" yaml:"Description,omitempty"`
SubjectNotes string `gorm:"type:TEXT;" json:"Notes,omitempty" yaml:"Notes,omitempty"`
Favorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
Hidden bool `json:"Hidden" yaml:"Hidden,omitempty"`
Private bool `json:"Private" yaml:"Private,omitempty"`
PhotoUID string `gorm:"type:VARBINARY(42);index;" json:"PhotoUID" yaml:"PhotoUID"`
PhotoCount int `gorm:"default:0" json:"PhotoCount" yaml:"-"`
MetadataJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"Metadata,omitempty" yaml:"Metadata,omitempty"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
SubjectUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
SubjectType string `gorm:"type:VARBINARY(8);default:''" json:"Type,omitempty" yaml:"Type,omitempty"`
SubjectSrc string `gorm:"type:VARBINARY(8);default:''" json:"Src,omitempty" yaml:"Src,omitempty"`
SubjectSlug string `gorm:"type:VARBINARY(255);index;default:''" json:"Slug" yaml:"-"`
SubjectName string `gorm:"type:VARCHAR(255);unique_index" json:"Name" yaml:"Name"`
SubjectBio string `gorm:"type:TEXT;default:''" json:"Bio" yaml:"Bio,omitempty"`
SubjectNotes string `gorm:"type:TEXT;default:''" json:"Notes,omitempty" yaml:"Notes,omitempty"`
Favorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
Private bool `json:"Private" yaml:"Private,omitempty"`
Excluded bool `json:"Excluded" yaml:"Excluded,omitempty"`
FileCount int `gorm:"default:0" json:"FileCount" yaml:"-"`
MetadataJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"Metadata,omitempty" yaml:"Metadata,omitempty"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
}
// UnknownPerson can be used as a placeholder for unknown people.
@@ -49,7 +50,9 @@ var UnknownPerson = Subject{
SubjectType: SubjectPerson,
SubjectSrc: SrcDefault,
Favorite: false,
PhotoCount: 0,
Private: false,
Excluded: false,
FileCount: 0,
}
// CreateUnknownPerson initializes the database with a placeholder for unknown people if not exists.
@@ -90,7 +93,7 @@ func NewSubject(name, subjectType, subjectSrc string) *Subject {
SubjectName: subjectName,
SubjectType: subjectType,
SubjectSrc: subjectSrc,
PhotoCount: 1,
FileCount: 1,
}
return result
@@ -148,7 +151,7 @@ func FirstOrCreateSubject(m *Subject) *Subject {
if err := UnscopedDb().Where("subject_name LIKE ?", m.SubjectName).First(&result).Error; err == nil {
return &result
} else if createErr := m.Create(); createErr == nil {
if !m.Hidden && m.SubjectType == SubjectPerson {
if !m.Excluded && m.SubjectType == SubjectPerson {
event.EntitiesCreated("people", []*Subject{m})
event.Publish("count.people", event.Data{

View File

@@ -20,106 +20,100 @@ func (m SubjectMap) Pointer(name string) *Subject {
var SubjectFixtures = SubjectMap{
"john-doe": Subject{
SubjectUID: "jqu0xs11qekk9jx8",
SubjectSlug: "john-doe",
SubjectName: "John Doe",
SubjectType: SubjectPerson,
SubjectSrc: SrcManual,
Favorite: true,
Private: false,
Hidden: false,
SubjectDescription: "Subject Description",
SubjectNotes: "Short Note",
MetadataJSON: []byte(""),
PhotoCount: 1,
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
SubjectUID: "jqu0xs11qekk9jx8",
SubjectSlug: "john-doe",
SubjectName: "John Doe",
SubjectType: SubjectPerson,
SubjectSrc: SrcManual,
Favorite: true,
Private: false,
Excluded: false,
SubjectBio: "Subject Description",
SubjectNotes: "Short Note",
MetadataJSON: []byte(""),
FileCount: 1,
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
},
"joe-biden": Subject{
SubjectUID: "jqy3y652h8njw0sx",
SubjectSlug: "joe-biden",
SubjectName: "Joe Biden",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,
Private: false,
Hidden: false,
SubjectDescription: "",
SubjectNotes: "",
MetadataJSON: []byte(""),
PhotoCount: 1,
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
SubjectUID: "jqy3y652h8njw0sx",
SubjectSlug: "joe-biden",
SubjectName: "Joe Biden",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,
Private: false,
Excluded: false,
SubjectBio: "",
SubjectNotes: "",
MetadataJSON: []byte(""),
FileCount: 1,
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
},
"dangling": Subject{
SubjectUID: "jqy1y111h1njaaaa",
SubjectSlug: "dangling-subject",
SubjectName: "Dangling Subject",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,
Private: false,
Hidden: false,
SubjectDescription: "",
SubjectNotes: "",
MetadataJSON: []byte(""),
PhotoCount: 0,
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
SubjectUID: "jqy1y111h1njaaaa",
SubjectSlug: "dangling-subject",
SubjectName: "Dangling Subject",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,
Private: false,
Excluded: false,
SubjectBio: "",
SubjectNotes: "",
MetadataJSON: []byte(""),
FileCount: 0,
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
},
"jane-doe": Subject{
SubjectUID: "jqy1y111h1njaaab",
SubjectSlug: "jane-doe",
SubjectName: "Jane Doe",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,
Private: false,
Hidden: false,
SubjectDescription: "",
SubjectNotes: "",
MetadataJSON: []byte(""),
PhotoCount: 3,
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
SubjectUID: "jqy1y111h1njaaab",
SubjectSlug: "jane-doe",
SubjectName: "Jane Doe",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,
Private: false,
Excluded: false,
SubjectBio: "",
SubjectNotes: "",
MetadataJSON: []byte(""),
FileCount: 3,
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
},
"actress-1": Subject{
SubjectUID: "jqy1y111h1njaaac",
SubjectSlug: "actress-a",
SubjectName: "Actress A",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,
Private: false,
Hidden: false,
SubjectDescription: "",
SubjectNotes: "",
MetadataJSON: []byte(""),
PhotoCount: 3,
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
SubjectUID: "jqy1y111h1njaaac",
SubjectSlug: "actress-a",
SubjectName: "Actress A",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,
Private: false,
SubjectNotes: "",
MetadataJSON: []byte(""),
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
},
"actor-1": Subject{
SubjectUID: "jqy1y111h1njaaad",
SubjectSlug: "actor-a",
SubjectName: "Actor A",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,
Private: false,
Hidden: false,
SubjectDescription: "",
SubjectNotes: "",
MetadataJSON: []byte(""),
PhotoCount: 4,
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
SubjectUID: "jqy1y111h1njaaad",
SubjectSlug: "actor-a",
SubjectName: "Actor A",
SubjectType: SubjectPerson,
SubjectSrc: SrcMarker,
Favorite: false,
Private: false,
SubjectNotes: "",
MetadataJSON: []byte(""),
CreatedAt: TimeStamp(),
UpdatedAt: TimeStamp(),
DeletedAt: nil,
},
}

View File

@@ -4,8 +4,8 @@ import "github.com/ulule/deepcopier"
// Album represents an album edit form.
type Album struct {
CoverUID string `json:"CoverUID"`
FolderUID string `json:"FolderUID"`
Thumb string `json:"Thumb"`
ThumbSrc string `json:"ThumbSrc"`
AlbumType string `json:"Type"`
AlbumTitle string `json:"Title"`
AlbumLocation string `json:"Location"`

View File

@@ -8,10 +8,10 @@ import (
"runtime/debug"
"strings"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fastwalk"
@@ -128,12 +128,22 @@ func (w *CleanUp) Start(opt CleanUpOptions) (thumbs int, orphans int, err error)
log.Errorf("cleanup: %s (purge orphans)", err)
}
// Update counts and views if needed.
if len(deleted) > 0 {
log.Info("cleanup: updating photo counts")
// Update photo counts and visibilities.
if err := entity.UpdatePhotoCounts(); err != nil {
log.Errorf("cleanup: %s", err)
log.Errorf("cleanup: %s (update counts)", err)
}
log.Info("cleanup: updating preview images")
// Update album, label, and subject preview images.
if err := query.UpdatePreviews(); err != nil {
log.Errorf("cleanup: %s (update previews)", err)
}
// Show success notification.
event.EntitiesDeleted("photos", deleted)
}

View File

@@ -245,8 +245,9 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
log.Errorf("import: %s", err)
}
// Update photo counts and visibilities.
if err := entity.UpdatePhotoCounts(); err != nil {
log.Errorf("import: %s", err)
log.Errorf("import: %s (update counts)", err)
}
}

View File

@@ -245,8 +245,9 @@ func (ind *Index) Start(opt IndexOptions) fs.Done {
"step": "counts",
})
// Update photo counts and visibilities.
if err := entity.UpdatePhotoCounts(); err != nil {
log.Errorf("index: %s", err)
log.Errorf("index: %s (update counts)", err)
}
} else {
log.Infof("index: no new or modified files")

View File

@@ -230,10 +230,6 @@ func (w *Moments) Start() (err error) {
log.Errorf("moments: %s (update album dates)", err.Error())
}
if err := query.ToggleMonthAlbums(); err != nil {
log.Errorf("moments: %s (toggle month albums)", err.Error())
}
if count, err := BackupAlbums(w.conf.AlbumsPath(), false); err != nil {
log.Errorf("moments: %s (backup albums)", err.Error())
} else if count > 0 {

View File

@@ -259,8 +259,18 @@ func (w *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPhot
log.Errorf("purge: %s (album entries)", err)
}
log.Info("purge: updating photo counts")
// Update photo counts and visibilities.
if err := entity.UpdatePhotoCounts(); err != nil {
log.Errorf("purge: %s (photo counts)", err)
log.Errorf("purge: %s (update counts)", err)
}
log.Info("purge: updating preview images")
// Update album, label, and subject preview image hashes.
if err := query.UpdatePreviews(); err != nil {
log.Errorf("purge: %s (update previews)", err)
}
return purgedFiles, purgedPhotos, nil

View File

@@ -15,8 +15,9 @@ import (
type AlbumResult struct {
ID uint `json:"-"`
AlbumUID string `json:"UID"`
CoverUID string `json:"CoverUID"`
FolderUID string `json:"FolderUID"`
ParentUID string `json:"ParentUID"`
Thumb string `json:"Thumb"`
ThumbSrc string `json:"ThumbSrc"`
AlbumSlug string `json:"Slug"`
AlbumType string `json:"Type"`
AlbumTitle string `json:"Title"`
@@ -186,26 +187,6 @@ func AlbumSearch(f form.AlbumSearch) (results AlbumResults, err error) {
return results, nil
}
// ToggleMonthAlbums toggles the visibility of calendar albums.
func ToggleMonthAlbums() (err error) {
switch DbDialect() {
default:
if err = UnscopedDb().Exec(`UPDATE albums SET deleted_at = ? WHERE album_type=? AND id NOT IN (
SELECT a.id FROM albums a JOIN photos p ON a.album_month = p.photo_month AND a.album_year = p.photo_year
AND p.deleted_at IS NULL AND p.photo_quality > -1 AND p.photo_private = 0 WHERE album_type=?)`,
entity.TimeStamp(), entity.AlbumMonth, entity.AlbumMonth).Error; err != nil {
return err
}
if err = UnscopedDb().Exec(`UPDATE albums SET deleted_at = NULL WHERE album_type=? AND id IN (
SELECT a.id FROM albums a JOIN photos p ON a.album_month = p.photo_month AND a.album_year = p.photo_year
AND p.deleted_at IS NULL AND p.photo_quality > -1 AND p.photo_private = 0 WHERE album_type=?)`, entity.AlbumMonth, entity.AlbumMonth).Error; err != nil {
return err
}
}
return nil
}
// UpdateAlbumDates updates album year, month and day based on indexed photo metadata.
func UpdateAlbumDates() error {
switch DbDialect() {

View File

@@ -207,14 +207,6 @@ func TestAlbumSearch(t *testing.T) {
})
}
func TestToggleMonthAlbums(t *testing.T) {
t.Run("success", func(t *testing.T) {
if err := ToggleMonthAlbums(); err != nil {
t.Fatal(err)
}
})
}
func TestUpdateAlbumDates(t *testing.T) {
t.Run("success", func(t *testing.T) {
if err := UpdateAlbumDates(); err != nil {

View File

@@ -9,6 +9,8 @@ type LabelResult struct {
// Label
ID uint `json:"ID"`
LabelUID string `json:"UID"`
Thumb string `json:"Thumb"`
ThumbSrc string `json:"ThumbSrc"`
LabelSlug string `json:"Slug"`
CustomSlug string `json:"CustomSlug"`
LabelName string `json:"Name"`

View File

@@ -126,7 +126,6 @@ func (m PhotoResults) Merged() (PhotoResults, int, error) {
}
file.ID = res.FileID
res.CompositeID = fmt.Sprintf("%d-%d", res.ID, res.FileID)
if lastId == res.ID && i > 0 {
merged[i-1].Files = append(merged[i-1].Files, file)
@@ -134,11 +133,12 @@ func (m PhotoResults) Merged() (PhotoResults, int, error) {
continue
}
lastId = res.ID
res.CompositeID = fmt.Sprintf("%d-%d", res.ID, res.FileID)
res.Files = append(res.Files, file)
merged = append(merged, res)
lastId = res.ID
i++
}

133
internal/query/previews.go Normal file
View File

@@ -0,0 +1,133 @@
package query
import (
"fmt"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/entity"
)
// UpdateAlbumDefaultPreviews updates default album preview images.
func UpdateAlbumDefaultPreviews() error {
return Db().Table(entity.Album{}.TableName()).
UpdateColumn("thumb", gorm.Expr(`(
SELECT file_hash FROM files f
JOIN photos_albums pa ON pa.album_uid = albums.album_uid AND pa.photo_uid = f.photo_uid AND pa.hidden = 0
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
WHERE f.deleted_at IS NULL AND f.file_missing = 0 AND f.file_hash <> '' AND f.file_primary = 1 AND f.file_type = 'jpg'
ORDER BY p.taken_at DESC LIMIT 1
) WHERE thumb_src='' AND album_type = 'album' AND deleted_at IS NULL`)).Error
}
// UpdateAlbumFolderPreviews updates folder album preview images.
func UpdateAlbumFolderPreviews() error {
return Db().Table(entity.Album{}.TableName()).
UpdateColumn("thumb", gorm.Expr(`(
SELECT file_hash FROM files f
JOIN photos p ON p.id = f.photo_id AND p.photo_path = albums.album_path AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
WHERE f.deleted_at IS NULL AND f.file_hash <> '' AND f.file_missing = 0 AND f.file_primary = 1 AND f.file_type = 'jpg'
ORDER BY p.taken_at DESC LIMIT 1
) WHERE thumb_src = '' AND album_type = 'folder' AND deleted_at IS NULL`)).
Error
}
// UpdateAlbumMonthPreviews updates month album preview images.
func UpdateAlbumMonthPreviews() error {
return Db().Table(entity.Album{}.TableName()).
UpdateColumn("thumb", gorm.Expr(`(
SELECT file_hash FROM files f
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
AND p.photo_year = albums.album_year AND p.photo_month = albums.album_month AND p.photo_month = albums.album_month
WHERE f.deleted_at IS NULL AND f.file_hash <> '' AND f.file_missing = 0 AND f.file_primary = 1 AND f.file_type = 'jpg'
ORDER BY p.taken_at DESC LIMIT 1
) WHERE thumb_src = '' AND album_type = 'month' AND deleted_at IS NULL`)).
Error
}
// UpdateAlbumPreviews updates album preview images.
func UpdateAlbumPreviews() (err error) {
// Update Default Albums.
if err = UpdateAlbumDefaultPreviews(); err != nil {
return err
}
// Update Folder Albums.
if err = UpdateAlbumFolderPreviews(); err != nil {
return err
}
// Update Monthly Albums.
if err = UpdateAlbumMonthPreviews(); err != nil {
return err
}
return nil
}
// UpdateLabelPreviews updates label preview images.
func UpdateLabelPreviews() (err error) {
// Labels.
if err = Db().Table(entity.Label{}.TableName()).
UpdateColumn("thumb", gorm.Expr(`(
SELECT file_hash FROM files f
JOIN photos_labels pl ON pl.label_id = labels.id AND pl.photo_id = f.photo_id AND pl.uncertainty < 100
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
WHERE f.deleted_at IS NULL AND f.file_hash <> '' AND f.file_missing = 0 AND f.file_primary = 1 AND f.file_type = 'jpg'
ORDER BY p.photo_quality DESC, pl.uncertainty ASC, p.taken_at DESC LIMIT 1
) WHERE thumb_src = '' AND deleted_at IS NULL`)).
Error; err != nil {
return err
}
// Categories.
if err = Db().Table(entity.Label{}.TableName()).
UpdateColumn("thumb", gorm.Expr(`(
SELECT file_hash FROM files f
JOIN photos_labels pl ON pl.photo_id = f.photo_id AND pl.uncertainty < 100
JOIN categories c ON c.label_id = pl.label_id AND c.category_id = labels.id
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
WHERE f.deleted_at IS NULL AND f.file_hash <> '' AND f.file_missing = 0 AND f.file_primary = 1 AND f.file_type = 'jpg'
ORDER BY p.photo_quality DESC, pl.uncertainty ASC, p.taken_at DESC LIMIT 1
) WHERE thumb IS NULL AND thumb_src = '' AND deleted_at IS NULL`)).
Error; err != nil {
return err
}
return nil
}
// UpdateSubjectPreviews updates subject preview images.
func UpdateSubjectPreviews() error {
return Db().Table(entity.Subject{}.TableName()).
UpdateColumn("thumb", gorm.Expr("(SELECT file_hash FROM files f "+
fmt.Sprintf(
"JOIN %s m ON f.id = m.file_id AND m.subject_uid = %s.subject_uid",
entity.Marker{}.TableName(),
entity.Subject{}.TableName())+
` JOIN photos p ON f.photo_id = p.id
WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL AND f.file_hash <> '' AND p.deleted_at IS NULL
AND f.file_primary = 1 AND f.file_missing = 0 AND p.photo_private = 0 AND p.photo_quality > -1
ORDER BY p.taken_at DESC LIMIT 1)
WHERE thumb_src='' AND deleted_at IS NULL AND subject_src <> 'default'`)).
Error
}
// UpdatePreviews updates album, labels, and subject preview images.
func UpdatePreviews() (err error) {
// Update Albums.
if err = UpdateAlbumPreviews(); err != nil {
return err
}
// Update Labels, and Categories.
if err = UpdateLabelPreviews(); err != nil {
return err
}
// Update Subjects.
if err = UpdateSubjectPreviews(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,35 @@
package query
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUpdateAlbumDefaultPreviews(t *testing.T) {
assert.NoError(t, UpdateAlbumDefaultPreviews())
}
func TestUpdateAlbumFolderPreviews(t *testing.T) {
assert.NoError(t, UpdateAlbumFolderPreviews())
}
func TestUpdateAlbumMonthPreviews(t *testing.T) {
assert.NoError(t, UpdateAlbumMonthPreviews())
}
func TestUpdateAlbumPreviews(t *testing.T) {
assert.NoError(t, UpdateAlbumPreviews())
}
func TestUpdateLabelPreviews(t *testing.T) {
assert.NoError(t, UpdateLabelPreviews())
}
func TestUpdateSubjectPreviews(t *testing.T) {
assert.NoError(t, UpdateSubjectPreviews())
}
func TestUpdatePreviews(t *testing.T) {
assert.NoError(t, UpdatePreviews())
}

View File

@@ -108,28 +108,37 @@ func (m *Meta) Start(delay time.Duration) (err error) {
// Explicitly set quality of photos without primary file to -1.
if err := query.ResetPhotoQuality(); err != nil {
log.Warnf("metadata: %s (reset photo quality)", err.Error())
log.Warnf("metadata: %s (reset quality)", err.Error())
}
// Update photo counts for labels and places.
log.Debugf("metadata: updating photo counts")
// Update photo counts and visibilities.
if err := entity.UpdatePhotoCounts(); err != nil {
log.Warnf("metadata: %s (update photo counts)", err.Error())
log.Warnf("metadata: %s (update counts)", err.Error())
}
// Run moments worker.
if w := photoprism.NewMoments(m.conf); w == nil {
log.Errorf("moments: failed creating worker")
log.Errorf("metadata: failed updating moments")
} else if err := w.Start(); err != nil {
log.Warnf("moments: %s", err)
log.Warn(err)
}
log.Debugf("metadata: running facial recognition")
// Run faces worker.
if w := photoprism.NewFaces(m.conf); w == nil {
log.Errorf("faces: failed creating worker")
} else if w.Disabled() {
// Do nothing.
if w := photoprism.NewFaces(m.conf); w.Disabled() {
log.Debugf("metadata: skipping facial recognition")
} else if err := w.Start(photoprism.FacesOptions{}); err != nil {
log.Warnf("faces: %s", err)
log.Warn(err)
}
log.Debugf("metadata: updating preview images")
// Update album, label, and subject preview image hashes.
if err := query.UpdatePreviews(); err != nil {
log.Errorf("metadata: %s (update previews)", err)
}
// Run garbage collection.

View File

@@ -181,6 +181,7 @@ func (worker *Sync) download(a entity.Account) (complete bool, err error) {
if len(done) > 0 {
worker.logError(entity.UpdatePhotoCounts())
worker.logError(query.UpdatePreviews())
}
return false, nil