mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
People: Refactor album, subject, and label previews #22
This commit is contained in:
48
frontend/package-lock.json
generated
48
frontend/package-lock.json
generated
@@ -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
@@ -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 ""
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
128
frontend/src/model/subject.js
Normal file
128
frontend/src/model/subject.js
Normal 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;
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
133
internal/query/previews.go
Normal 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
|
||||
}
|
||||
35
internal/query/previews_test.go
Normal file
35
internal/query/previews_test.go
Normal 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())
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user