From dd9d7123d9ba4ad84621585df13794789ac623c3 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Wed, 5 Jan 2022 16:37:19 +0100 Subject: [PATCH] Index: Improve indexing and unstacking of related files #1823 This commit also adds initial HDR flag extraction from metadata. --- frontend/package-lock.json | 125 +++++++++++-------- frontend/src/dialog/photo/files.vue | 8 ++ frontend/src/locales/translations.pot | 49 ++++---- frontend/src/model/file.js | 1 + internal/api/photo_unstack.go | 2 + internal/entity/file.go | 18 +++ internal/entity/file_json.go | 2 + internal/entity/file_test.go | 14 +++ internal/entity/photo.go | 2 +- internal/entity/photo_merge.go | 2 +- internal/meta/data.go | 12 +- internal/meta/json_exiftool.go | 6 +- internal/meta/keywords.go | 3 + internal/photoprism/import_worker.go | 19 +-- internal/photoprism/index_mediafile.go | 103 +++++++++++----- internal/photoprism/index_mediafile_test.go | 10 +- internal/photoprism/index_related.go | 29 +++-- internal/photoprism/mediafile.go | 23 +++- internal/photoprism/related.go | 31 ++++- internal/photoprism/related_test.go | 128 +++++++++++++++++++- internal/query/files.go | 2 +- 21 files changed, 452 insertions(+), 137 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 357ff0a4c..f14aa0abe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1910,9 +1910,9 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, "node_modules/@types/node": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.7.tgz", - "integrity": "sha512-1QUk+WAUD4t8iR+Oj+UgI8oJa6yyxaB8a8pHaC8uqM6RrS1qbL7bf3Pwl5rHv0psm2CuDErgho6v5N+G+5fwtQ==" + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", + "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -3052,9 +3052,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001295", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001295.tgz", - "integrity": "sha512-lSP16vcyC0FEy0R4ECc9duSPoKoZy+YkpGkue9G4D81OfPnliopaZrU10+qtPdT8PbGXad/PNx43TIQrOmJZSQ==", + "version": "1.0.30001296", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz", + "integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==", "funding": { "type": "opencollective", "url": "https://opencollective.com/browserslist" @@ -3819,9 +3819,9 @@ "integrity": "sha1-s085HupNqPPpgjHizNjfnAQfFxs=" }, "node_modules/cssdb": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-5.0.0.tgz", - "integrity": "sha512-Q7982SynYCtcLUBCPgUPFy2TZmDiFyimpdln8K2v4w2c07W4rXL7q5F1ksVAqOAQfxKyyUGCKSsioezKT5bU1Q==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-5.1.0.tgz", + "integrity": "sha512-/vqjXhv1x9eGkE/zO6o8ZOI7dgdZbLVLUGyVRbPgk6YipXbW87YzUCcO+Jrmi5bwJlAH6oD+MNeZyRgXea1GZw==" }, "node_modules/cssesc": { "version": "3.0.0", @@ -4254,9 +4254,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.31", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.31.tgz", - "integrity": "sha512-t3XVQtk+Frkv6aTD4RRk0OqosU+VLe1dQFW83MDer78ZD6a52frgXuYOIsLYTQiH2Lm+JB2OKYcn7zrX+YGAiQ==" + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.35.tgz", + "integrity": "sha512-wzTOMh6HGFWeALMI3bif0mzgRrVGyP1BdFRx7IvWukFrSC5QVQELENuy+Fm2dCrAdQH9T3nuqr07n94nPDFBWA==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -6095,9 +6095,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, "node_modules/grid-index": { "version": "1.1.0", @@ -7060,9 +7060,9 @@ } }, "node_modules/jest-worker": { - "version": "27.4.5", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.5.tgz", - "integrity": "sha512-f2s8kEdy15cv9r7q4KkzGXvlY0JTcmCbMHZBfSQDwW77REr45IDWwd0lksDFeVHH2jJ5pqb90T77XscrjeGzzg==", + "version": "27.4.6", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.6.tgz", + "integrity": "sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw==", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -10069,12 +10069,16 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", + "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.8.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11099,6 +11103,17 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/svg-url-loader": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/svg-url-loader/-/svg-url-loader-7.1.1.tgz", @@ -11172,9 +11187,9 @@ } }, "node_modules/table": { - "version": "6.7.5", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", - "integrity": "sha512-LFNeryOqiQHqCVKzhkymKwt6ozeRhlm8IL1mE8rNUurkir4heF6PzMyRgaTa4tlyPTGGgXuvVOF/OLWiH09Lqw==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", + "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", @@ -13866,9 +13881,9 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, "@types/node": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.7.tgz", - "integrity": "sha512-1QUk+WAUD4t8iR+Oj+UgI8oJa6yyxaB8a8pHaC8uqM6RrS1qbL7bf3Pwl5rHv0psm2CuDErgho6v5N+G+5fwtQ==" + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", + "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==" }, "@types/parse-json": { "version": "4.0.0", @@ -14759,9 +14774,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001295", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001295.tgz", - "integrity": "sha512-lSP16vcyC0FEy0R4ECc9duSPoKoZy+YkpGkue9G4D81OfPnliopaZrU10+qtPdT8PbGXad/PNx43TIQrOmJZSQ==" + "version": "1.0.30001296", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz", + "integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==" }, "chai": { "version": "4.3.4", @@ -15324,9 +15339,9 @@ "integrity": "sha1-s085HupNqPPpgjHizNjfnAQfFxs=" }, "cssdb": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-5.0.0.tgz", - "integrity": "sha512-Q7982SynYCtcLUBCPgUPFy2TZmDiFyimpdln8K2v4w2c07W4rXL7q5F1ksVAqOAQfxKyyUGCKSsioezKT5bU1Q==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-5.1.0.tgz", + "integrity": "sha512-/vqjXhv1x9eGkE/zO6o8ZOI7dgdZbLVLUGyVRbPgk6YipXbW87YzUCcO+Jrmi5bwJlAH6oD+MNeZyRgXea1GZw==" }, "cssesc": { "version": "3.0.0", @@ -15643,9 +15658,9 @@ "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==" }, "electron-to-chromium": { - "version": "1.4.31", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.31.tgz", - "integrity": "sha512-t3XVQtk+Frkv6aTD4RRk0OqosU+VLe1dQFW83MDer78ZD6a52frgXuYOIsLYTQiH2Lm+JB2OKYcn7zrX+YGAiQ==" + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.35.tgz", + "integrity": "sha512-wzTOMh6HGFWeALMI3bif0mzgRrVGyP1BdFRx7IvWukFrSC5QVQELENuy+Fm2dCrAdQH9T3nuqr07n94nPDFBWA==" }, "emoji-regex": { "version": "8.0.0", @@ -16995,9 +17010,9 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, "grid-index": { "version": "1.1.0", @@ -17649,9 +17664,9 @@ } }, "jest-worker": { - "version": "27.4.5", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.5.tgz", - "integrity": "sha512-f2s8kEdy15cv9r7q4KkzGXvlY0JTcmCbMHZBfSQDwW77REr45IDWwd0lksDFeVHH2jJ5pqb90T77XscrjeGzzg==", + "version": "27.4.6", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.6.tgz", + "integrity": "sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw==", "requires": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -19812,12 +19827,13 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", + "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.8.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, "resolve-cwd": { @@ -20620,6 +20636,11 @@ } } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, "svg-url-loader": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/svg-url-loader/-/svg-url-loader-7.1.1.tgz", @@ -20673,9 +20694,9 @@ } }, "table": { - "version": "6.7.5", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", - "integrity": "sha512-LFNeryOqiQHqCVKzhkymKwt6ozeRhlm8IL1mE8rNUurkir4heF6PzMyRgaTa4tlyPTGGgXuvVOF/OLWiH09Lqw==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", + "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", "requires": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", diff --git a/frontend/src/dialog/photo/files.vue b/frontend/src/dialog/photo/files.vue index 8d58c6027..3dee55ff0 100644 --- a/frontend/src/dialog/photo/files.vue +++ b/frontend/src/dialog/photo/files.vue @@ -154,6 +154,14 @@ {{ file.Orientation }} + + + HDR + + + Yes + + Color Profile diff --git a/frontend/src/locales/translations.pot b/frontend/src/locales/translations.pot index e4b77e3f0..615e7acb0 100644 --- a/frontend/src/locales/translations.pot +++ b/frontend/src/locales/translations.pot @@ -125,8 +125,8 @@ msgstr "" msgid "Add to album" msgstr "" -#: src/dialog/photo/files.vue:163 -#: src/dialog/photo/files.vue:160 +#: src/dialog/photo/files.vue:171 +#: src/dialog/photo/files.vue:168 msgid "Added" msgstr "" @@ -491,8 +491,8 @@ msgstr "" msgid "Chinese Traditional" msgstr "" -#: src/dialog/photo/files.vue:149 -#: src/dialog/photo/files.vue:146 +#: src/dialog/photo/files.vue:157 +#: src/dialog/photo/files.vue:154 msgid "Chroma" msgstr "" @@ -512,8 +512,8 @@ msgstr "" msgid "Color" msgstr "" -#: src/dialog/photo/files.vue:137 -#: src/dialog/photo/files.vue:134 +#: src/dialog/photo/files.vue:145 +#: src/dialog/photo/files.vue:142 msgid "Color Profile" msgstr "" @@ -987,7 +987,7 @@ msgstr "" msgid "Feel free to contact us at hello@photoprism.app if you have any questions." msgstr "" -#: src/model/file.js:245 +#: src/model/file.js:246 msgid "File" msgstr "" @@ -1080,6 +1080,11 @@ msgstr "" msgid "Hash" msgstr "" +#: src/dialog/photo/files.vue:137 +#: src/dialog/photo/files.vue:134 +msgid "HDR" +msgstr "" + #: src/options/options.js:115 msgid "Hebrew" msgstr "" @@ -1163,10 +1168,10 @@ msgstr "" msgid "Importing files to originals…" msgstr "" -#: src/dialog/photo/files.vue:166 -#: src/dialog/photo/files.vue:175 -#: src/dialog/photo/files.vue:163 -#: src/dialog/photo/files.vue:172 +#: src/dialog/photo/files.vue:174 +#: src/dialog/photo/files.vue:183 +#: src/dialog/photo/files.vue:171 +#: src/dialog/photo/files.vue:180 msgid "in" msgstr "" @@ -1392,8 +1397,8 @@ msgstr "" msgid "Magenta" msgstr "" -#: src/dialog/photo/files.vue:143 -#: src/dialog/photo/files.vue:140 +#: src/dialog/photo/files.vue:151 +#: src/dialog/photo/files.vue:148 msgid "Main Color" msgstr "" @@ -1425,8 +1430,8 @@ msgstr "" msgid "Minimize" msgstr "" -#: src/dialog/photo/files.vue:155 -#: src/dialog/photo/files.vue:152 +#: src/dialog/photo/files.vue:163 +#: src/dialog/photo/files.vue:160 msgid "Missing" msgstr "" @@ -2218,7 +2223,7 @@ msgstr "" msgid "Shows more detailed log messages. Requires a restart." msgstr "" -#: src/model/file.js:186 +#: src/model/file.js:187 msgid "Sidecar" msgstr "" @@ -2500,8 +2505,8 @@ msgstr "" msgid "Unstack" msgstr "" -#: src/dialog/photo/files.vue:172 -#: src/dialog/photo/files.vue:169 +#: src/dialog/photo/files.vue:180 +#: src/dialog/photo/files.vue:177 #: src/dialog/photo/info.vue:175 msgid "Updated" msgstr "" @@ -2603,7 +2608,7 @@ msgstr "" #: src/component/photo/cards.vue:225 #: src/component/photo/list.vue:196 #: src/component/photo/mosaic.vue:200 -#: src/model/file.js:184 +#: src/model/file.js:185 #: src/model/photo.js:662 #: src/model/photo.js:676 #: src/options/options.js:328 @@ -2668,10 +2673,12 @@ msgstr "" #: src/dialog/photo/archive.vue:18 #: src/dialog/photo/files.vue:104 #: src/dialog/photo/files.vue:112 -#: src/dialog/photo/files.vue:158 +#: src/dialog/photo/files.vue:140 +#: src/dialog/photo/files.vue:166 #: src/dialog/photo/files.vue:101 #: src/dialog/photo/files.vue:109 -#: src/dialog/photo/files.vue:155 +#: src/dialog/photo/files.vue:137 +#: src/dialog/photo/files.vue:163 #: src/dialog/photo/info.vue:284 #: src/dialog/photo/info.vue:305 #: src/dialog/photo/info.vue:325 diff --git a/frontend/src/model/file.js b/frontend/src/model/file.js index 8cbc5941c..c56358921 100644 --- a/frontend/src/model/file.js +++ b/frontend/src/model/file.js @@ -62,6 +62,7 @@ export class File extends RestModel { Orientation: 0, Projection: "", AspectRatio: 1.0, + HDR: false, ColorProfile: "", MainColor: "", Colors: "", diff --git a/internal/api/photo_unstack.go b/internal/api/photo_unstack.go index 112502de1..5aeb1e688 100644 --- a/internal/api/photo_unstack.go +++ b/internal/api/photo_unstack.go @@ -17,6 +17,8 @@ import ( "github.com/photoprism/photoprism/internal/service" ) +// PhotoUnstack removes a file from an existing photo stack. +// // POST /api/v1/photos/:uid/files/:file_uid/unstack // // Parameters: diff --git a/internal/entity/file.go b/internal/entity/file.go index c91349692..ebff1ccb9 100644 --- a/internal/entity/file.go +++ b/internal/entity/file.go @@ -57,6 +57,7 @@ type File struct { FileOrientation int `json:"Orientation" yaml:"Orientation,omitempty"` FileProjection string `gorm:"type:VARBINARY(40);" json:"Projection,omitempty" yaml:"Projection,omitempty"` FileAspectRatio float32 `gorm:"type:FLOAT;" json:"AspectRatio" yaml:"AspectRatio,omitempty"` + FileHDR bool `gorm:"column:file_hdr;" json:"IsHDR" yaml:"IsHDR,omitempty"` FileColorProfile string `gorm:"type:VARBINARY(40);" json:"ColorProfile,omitempty" yaml:"ColorProfile,omitempty"` FileMainColor string `gorm:"type:VARBINARY(16);index;" json:"MainColor" yaml:"MainColor,omitempty"` FileColors string `gorm:"type:VARBINARY(9);" json:"Colors" yaml:"Colors,omitempty"` @@ -478,6 +479,23 @@ func (m *File) SetProjection(name string) { m.FileProjection = SanitizeTypeString(name) } +// IsHDR returns true if it is a high dynamic range file. +func (m *File) IsHDR() bool { + return m.FileHDR +} + +// SetHDR sets the high dynamic range flag. +func (m *File) SetHDR(isHdr bool) { + if isHdr { + m.FileHDR = true + } +} + +// ResetHDR removes the high dynamic range flag. +func (m *File) ResetHDR() { + m.FileHDR = false +} + // ColorProfile returns the ICC color profile name if any. func (m *File) ColorProfile() string { return SanitizeTypeCaseSensitive(m.FileColorProfile) diff --git a/internal/entity/file_json.go b/internal/entity/file_json.go index 7b80fb289..178892e86 100644 --- a/internal/entity/file_json.go +++ b/internal/entity/file_json.go @@ -36,6 +36,7 @@ func (m *File) MarshalJSON() ([]byte, error) { Luminance string `json:",omitempty"` Diff uint32 `json:",omitempty"` Chroma uint8 `json:",omitempty"` + HDR bool `json:",omitempty"` Error string `json:",omitempty"` ModTime int64 `json:",omitempty"` CreatedAt time.Time `json:",omitempty"` @@ -73,6 +74,7 @@ func (m *File) MarshalJSON() ([]byte, error) { Luminance: m.FileLuminance, Diff: m.FileDiff, Chroma: m.FileChroma, + HDR: m.FileHDR, Error: m.FileError, ModTime: m.ModTime, CreatedAt: m.CreatedAt, diff --git a/internal/entity/file_test.go b/internal/entity/file_test.go index a89263113..25d20f437 100644 --- a/internal/entity/file_test.go +++ b/internal/entity/file_test.go @@ -628,6 +628,20 @@ func TestFile_ReplaceHash(t *testing.T) { }) } +func TestFile_SetHDR(t *testing.T) { + t.Run("Success", func(t *testing.T) { + m := FileFixtures.Get("exampleFileName.jpg") + + assert.Equal(t, false, m.IsHDR()) + m.SetHDR(false) + assert.Equal(t, false, m.IsHDR()) + m.SetHDR(true) + assert.Equal(t, true, m.IsHDR()) + m.ResetHDR() + assert.Equal(t, false, m.IsHDR()) + }) +} + func TestFile_SetColorProfile(t *testing.T) { t.Run("DisplayP3", func(t *testing.T) { m := FileFixtures.Get("exampleFileName.jpg") diff --git a/internal/entity/photo.go b/internal/entity/photo.go index 38f017927..28dd6dfe6 100644 --- a/internal/entity/photo.go +++ b/internal/entity/photo.go @@ -863,7 +863,7 @@ func (m *Photo) SetPrimary(fileUID string) error { // Do nothing. } else if err := Db().Model(File{}). Where("photo_uid = ? AND file_type = 'jpg' AND file_missing = 0 AND file_error = ''", m.PhotoUID). - Order("file_width DESC").Limit(1). + Order("file_width DESC, file_hdr DESC").Limit(1). Pluck("file_uid", &files).Error; err != nil { return err } else if len(files) == 0 { diff --git a/internal/entity/photo_merge.go b/internal/entity/photo_merge.go index d5c5a486d..b42d0b1b4 100644 --- a/internal/entity/photo_merge.go +++ b/internal/entity/photo_merge.go @@ -13,7 +13,7 @@ var photoMergeMutex = sync.Mutex{} func (m *Photo) ResolvePrimary() error { var file File - if err := Db().Where("file_primary = 1 AND photo_id = ?", m.ID).First(&file).Error; err == nil && file.ID > 0 { + if err := Db().Where("file_primary = 1 AND photo_id = ?", m.ID).Order("file_width DESC, file_hdr DESC").First(&file).Error; err == nil && file.ID > 0 { return file.ResolvePrimary() } diff --git a/internal/meta/data.go b/internal/meta/data.go index 28a6362b8..79ccf4ca3 100644 --- a/internal/meta/data.go +++ b/internal/meta/data.go @@ -8,9 +8,13 @@ import ( "github.com/photoprism/photoprism/pkg/s2" ) +const ( + ImageTypeHDR = 3 // see https://exiftool.org/TagNames/Apple.html +) + // Data represents image meta data. type Data struct { - DocumentID string `meta:"ImageUniqueID,OriginalDocumentID,DocumentID"` + DocumentID string `meta:"BurstUUID,MediaGroupUUID,ImageUniqueID,OriginalDocumentID,DocumentID"` InstanceID string `meta:"InstanceID,DocumentID"` TakenAt time.Time `meta:"DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeDigitized,DateTime"` TakenAtLocal time.Time `meta:"DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeDigitized,DateTime"` @@ -38,6 +42,7 @@ type Data struct { Aperture float32 `meta:"ApertureValue"` FNumber float32 `meta:"FNumber"` Iso int `meta:"ISO"` + ImageType int `meta:"HDRImageType"` GPSPosition string `meta:"GPSPosition"` GPSLatitude string `meta:"GPSLatitude"` GPSLongitude string `meta:"GPSLongitude"` @@ -80,6 +85,11 @@ func (data Data) Portrait() bool { return data.ActualWidth() < data.ActualHeight() } +// IsHDR tests if it is a high dynamic range file. +func (data Data) IsHDR() bool { + return data.ImageType == ImageTypeHDR +} + // Megapixels returns the resolution in megapixels. func (data Data) Megapixels() int { return int(math.Round(float64(data.Width*data.Height) / 1000000)) diff --git a/internal/meta/json_exiftool.go b/internal/meta/json_exiftool.go index 7f6b8bd86..4508e8b13 100644 --- a/internal/meta/json_exiftool.go +++ b/internal/meta/json_exiftool.go @@ -272,8 +272,12 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) { data.AddKeywords(KeywordPanorama) } + if data.Description != "" { + data.AutoAddKeywords(data.Description) + data.Description = SanitizeDescription(data.Description) + } + data.Title = SanitizeTitle(data.Title) - data.Description = SanitizeDescription(data.Description) data.Subject = SanitizeMeta(data.Subject) data.Artist = SanitizeMeta(data.Artist) diff --git a/internal/meta/keywords.go b/internal/meta/keywords.go index 95ab1fc71..f96208459 100644 --- a/internal/meta/keywords.go +++ b/internal/meta/keywords.go @@ -46,6 +46,9 @@ func (data *Data) AutoAddKeywords(s string) { for _, w := range AutoKeywords { if strings.Contains(s, w) { data.AddKeywords(w) + if w == KeywordHdr { + data.ImageType = ImageTypeHDR + } } } } diff --git a/internal/photoprism/import_worker.go b/internal/photoprism/import_worker.go index d458d999b..a6116f226 100644 --- a/internal/photoprism/import_worker.go +++ b/internal/photoprism/import_worker.go @@ -4,10 +4,10 @@ import ( "os" "path/filepath" - "github.com/photoprism/photoprism/internal/query" - "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/internal/query" + "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/sanitize" ) @@ -159,6 +159,7 @@ func ImportWorker(jobs <-chan ImportJob) { done := make(map[string]bool) ind := imp.index sizeLimit := ind.conf.OriginalsLimit() + photoUID := "" if related.Main != nil { f := related.Main @@ -169,17 +170,19 @@ func ImportWorker(jobs <-chan ImportJob) { continue } - res := ind.MediaFile(f, indexOpt, originalName) + res := ind.MediaFile(f, indexOpt, originalName, "") log.Infof("import: %s main %s file %s", res, f.FileType(), sanitize.Log(f.RelName(ind.originalsPath()))) done[f.FileName()] = true - if res.Success() { - if err := entity.AddPhotoToAlbums(res.PhotoUID, opt.Albums); err != nil { + if !res.Success() { + continue + } else if res.PhotoUID != "" { + photoUID = res.PhotoUID + + if err := entity.AddPhotoToAlbums(photoUID, opt.Albums); err != nil { log.Warn(err) } - } else { - continue } } else { log.Warnf("import: found no main file for %s, conversion to jpeg may have failed", fs.RelName(destMainFileName, imp.originalsPath())) @@ -210,7 +213,7 @@ func ImportWorker(jobs <-chan ImportJob) { } } - res := ind.MediaFile(f, indexOpt, "") + res := ind.MediaFile(f, indexOpt, "", photoUID) if res.Indexed() && f.IsJpeg() { if err := f.ResampleDefault(ind.thumbPath(), false); err != nil { diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index 9089fcee1..443f95f99 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -21,7 +21,7 @@ import ( ) // MediaFile indexes a single media file. -func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (result IndexResult) { +func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID string) (result IndexResult) { if m == nil { err := errors.New("index: media file is nil - you might have found a bug") log.Error(err) @@ -130,48 +130,78 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( } } - // Look for existing photo if file wasn't indexed yet... - if !fileExists { + // Find existing photo if a photo uid was provided or file has not been indexed yet... + if photoUID != "" { + // Find existing photo by UID. + photoQuery = entity.UnscopedDb().First(&photo, "photo_uid = ?", photoUID) + + if photoQuery.Error == nil { + // Found. + fileStacked = true + } else { + // Log and return error if photo uid was not found. + log.Errorf("index: cannot add %s to unknown photo uid %s", logName, photoUID) + + result.Status = IndexFailed + result.Err = photoQuery.Error + + return result + } + } else if !fileExists { + // Find existing photo by matching path and name. if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fullBase); photoQuery.Error == nil || fileBase == fullBase || !o.Stack { // Skip next query. } else if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ? AND photo_stack > -1", filePath, fileBase); photoQuery.Error == nil { + // Found. fileStacked = true } - // Find existing photo stack? + // Find existing photo by unique id or time and location? if o.Stack { + // Same unique ID? + if photoQuery.Error != nil && Config().Settings().StackUUID() && m.MetaData().HasDocumentID() { + photoQuery = entity.UnscopedDb().First(&photo, "uuid <> '' AND uuid = ?", sanitize.Log(m.MetaData().DocumentID)) + + if photoQuery.Error == nil { + // Found. + fileStacked = true + } + } + // Matching location and time metadata? if photoQuery.Error != nil && Config().Settings().StackMeta() && m.MetaData().HasTimeAndPlace() { metaData = m.MetaData() photoQuery = entity.UnscopedDb().First(&photo, "photo_lat = ? AND photo_lng = ? AND taken_at = ? AND taken_src = 'meta' AND camera_serial = ?", metaData.Lat, metaData.Lng, metaData.TakenAt, metaData.CameraSerial) if photoQuery.Error == nil { - fileStacked = true - } - } - - // Same unique ID? - if photoQuery.Error != nil && Config().Settings().StackUUID() && m.MetaData().HasDocumentID() { - photoQuery = entity.UnscopedDb().First(&photo, "uuid <> '' AND uuid = ?", sanitize.Log(m.MetaData().DocumentID)) - - if photoQuery.Error == nil { - fileStacked = true - } - } - - // Related file? - if photoQuery.Error != nil { - photoQuery = entity.UnscopedDb().First(&photo, "id IN (SELECT photo_id FROM files WHERE file_sidecar = 0 AND file_missing = 0 AND file_name = LIKE ?)", fs.StripKnownExt(fileName)+".%") - - if photoQuery.Error == nil { - log.Debugf("index: %s belongs to %s", sanitize.Log(m.BaseName()), sanitize.Log(filepath.Join(photo.PhotoPath, photo.PhotoName))) + // Found. fileStacked = true } } } - } else { + } else if fileExists { + // Find photo by id if file exists. photoQuery = entity.UnscopedDb().First(&photo, "id = ?", file.PhotoID) + } else { + // Should never happen. + result.Status = IndexFailed + result.Err = fmt.Errorf("failed indexing %s - please report as this should never happen", logName) + return result + } + + // Found a photo? + photoExists = photoQuery.Error == nil + + // Detect changes in existing files. + if fileExists { + // Detect and report changed photo UID. + if photoExists && photoUID != "" && photoUID != file.PhotoUID { + fileChanged = true + log.Debugf("index: %s has new photo uid %s", sanitize.Log(m.BaseName()), photoUID) + } + + // Detect and report file changes. if fileRenamed { fileChanged = true log.Debugf("index: %s was renamed", sanitize.Log(m.BaseName())) @@ -184,8 +214,13 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( } } - photoExists = photoQuery.Error == nil + // Update file <=> photo relationship if needed. + if photoExists && (file.PhotoID != photo.ID || file.PhotoUID != photo.PhotoUID) { + file.PhotoID = photo.ID + file.PhotoUID = photo.PhotoUID + } + // Skip unchanged files. if !fileChanged && photoExists && o.SkipUnchanged() || !photoExists && m.IsSidecar() { result.Status = IndexSkipped return result @@ -196,6 +231,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( log.Error(err) } + // Fetch photo details such as keywords, subject, and artist. details := photo.GetDetails() // Try to recover photo metadata from backup if not exists. @@ -230,14 +266,18 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( log.Errorf("index: %s while updating covers of %s", err, logName) } - photo.PhotoPath = filePath + // Update photo path based on main file. + if photoUID == "" && !fileStacked { + photo.PhotoPath = filePath - if !o.Stack || !stripSequence || photo.PhotoStack == entity.IsUnstacked { - photo.PhotoName = fullBase - } else { - photo.PhotoName = fileBase + if !o.Stack || !stripSequence || photo.PhotoStack == entity.IsUnstacked { + photo.PhotoName = fullBase + } else { + photo.PhotoName = fileBase + } } + // Clear (previous) file error. file.FileError = "" // Flag first JPEG as primary file for this photo. @@ -343,6 +383,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( if metaData := m.MetaData(); metaData.Error == nil { file.FileCodec = metaData.Codec file.SetProjection(metaData.Projection) + file.SetHDR(metaData.IsHDR()) file.SetColorProfile(metaData.ColorProfile) if metaData.HasInstanceID() { @@ -403,6 +444,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( file.FileAspectRatio = m.AspectRatio() file.FilePortrait = m.Portrait() file.SetProjection(metaData.Projection) + file.SetHDR(metaData.IsHDR()) file.SetColorProfile(metaData.ColorProfile) if res := m.Megapixels(); res > photo.PhotoResolution { @@ -456,6 +498,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( file.FilePortrait = m.Portrait() file.FileDuration = metaData.Duration file.SetProjection(metaData.Projection) + file.SetHDR(metaData.IsHDR()) file.SetColorProfile(metaData.ColorProfile) if res := m.Megapixels(); res > photo.PhotoResolution { diff --git a/internal/photoprism/index_mediafile_test.go b/internal/photoprism/index_mediafile_test.go index ac04f9478..675d9cf8a 100644 --- a/internal/photoprism/index_mediafile_test.go +++ b/internal/photoprism/index_mediafile_test.go @@ -3,12 +3,12 @@ package photoprism import ( "testing" - "github.com/photoprism/photoprism/internal/face" + "github.com/stretchr/testify/assert" "github.com/photoprism/photoprism/internal/classify" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/face" "github.com/photoprism/photoprism/internal/nsfw" - "github.com/stretchr/testify/assert" ) func TestIndex_MediaFile(t *testing.T) { @@ -36,7 +36,7 @@ func TestIndex_MediaFile(t *testing.T) { assert.Equal(t, "", mediaFile.metaData.Keywords.String()) - result := ind.MediaFile(mediaFile, indexOpt, "flash.jpg") + result := ind.MediaFile(mediaFile, indexOpt, "flash.jpg", "") words := mediaFile.metaData.Keywords.String() @@ -66,7 +66,7 @@ func TestIndex_MediaFile(t *testing.T) { } assert.Equal(t, "", mediaFile.metaData.Title) - result := ind.MediaFile(mediaFile, indexOpt, "blue-go-video.mp4") + result := ind.MediaFile(mediaFile, indexOpt, "blue-go-video.mp4", "") assert.Equal(t, "Blue Gopher", mediaFile.metaData.Title) assert.Equal(t, IndexStatus("added"), result.Status) }) @@ -83,7 +83,7 @@ func TestIndex_MediaFile(t *testing.T) { ind := NewIndex(conf, tf, nd, fn, convert, NewFiles(), NewPhotos()) indexOpt := IndexOptionsAll() - result := ind.MediaFile(nil, indexOpt, "blue-go-video.mp4") + result := ind.MediaFile(nil, indexOpt, "blue-go-video.mp4", "") assert.Equal(t, IndexStatus("failed"), result.Status) }) } diff --git a/internal/photoprism/index_related.go b/internal/photoprism/index_related.go index 89eda8495..9d94bcb90 100644 --- a/internal/photoprism/index_related.go +++ b/internal/photoprism/index_related.go @@ -7,14 +7,15 @@ import ( "github.com/dustin/go-humanize/english" "github.com/photoprism/photoprism/internal/query" + "github.com/photoprism/photoprism/pkg/sanitize" ) // IndexMain indexes the main file from a group of related files and returns the result. func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result IndexResult) { - // Skip sidecar files without related media file. + // Skip if main file is nil. if related.Main == nil { - result.Err = fmt.Errorf("index: found no main file for %s", sanitize.Log(related.String())) + result.Err = fmt.Errorf("index: no main file for %s", sanitize.Log(related.String())) result.Status = IndexFailed return result } @@ -57,7 +58,7 @@ func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result Inde } } - result = ind.MediaFile(f, opt, "") + result = ind.MediaFile(f, opt, "", "") if result.Indexed() && f.IsJpeg() { if err := f.ResampleDefault(ind.thumbPath(), false); err != nil { @@ -73,6 +74,13 @@ func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result Inde // IndexRelated indexes a group of related files and returns the result. func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result IndexResult) { + // Skip if main file is nil. + if related.Main == nil { + result.Err = fmt.Errorf("index: no main file for %s", sanitize.Log(related.String())) + result.Status = IndexFailed + return result + } + done := make(map[string]bool) sizeLimit := ind.conf.OriginalsLimit() @@ -84,12 +92,15 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In } else if !result.Success() { // Skip related files if indexing was not completely successful. return result - } else if result.Stacked() && len(related.Files) > 1 && related.Main != nil { + } else if !result.Indexed() { + // Skip related files if main file was not indexed but for example skipped. + if related.Len() > 1 { + log.Warnf("index: %s main %s file %s has %s", result, related.MainFileType(), related.MainLogName(), english.Plural(related.Count(), "related file", "related files")) + } + return result + } else if result.Stacked() && related.Len() > 1 { // Show info if main file was stacked and has additional related files. - fileType := string(related.Main.FileType()) - relatedFiles := len(related.Files) - 1 - mainLogName := sanitize.Log(related.Main.RelName(ind.originalsPath())) - log.Infof("index: stacked main %s file %s has %s", fileType, mainLogName, english.Plural(relatedFiles, "related file", "related files")) + log.Infof("index: %s main %s file %s has %s", result, related.MainFileType(), related.MainLogName(), english.Plural(related.Count(), "related file", "related files")) } done[related.Main.FileName()] = true @@ -144,7 +155,7 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In } } - res := ind.MediaFile(f, opt, "") + res := ind.MediaFile(f, opt, "", result.PhotoUID) if res.Indexed() && f.IsJpeg() { if err := f.ResampleDefault(ind.thumbPath(), false); err != nil { diff --git a/internal/photoprism/mediafile.go b/internal/photoprism/mediafile.go index dd66fcc24..facb9365c 100644 --- a/internal/photoprism/mediafile.go +++ b/internal/photoprism/mediafile.go @@ -278,14 +278,26 @@ func (m *MediaFile) EditedName() string { // RelatedFiles returns files which are related to this file. func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err error) { - var prefix string + // File path and name without any extensions. + prefix := m.AbsPrefix(stripSequence) + // Storage folder path prefixes. + sidecarPrefix := Config().SidecarPath() + "/" + originalsPrefix := Config().OriginalsPath() + "/" + + // Replace sidecar with originals path in search prefix. + if len(sidecarPrefix) > 1 && sidecarPrefix != originalsPrefix && strings.HasPrefix(prefix, sidecarPrefix) { + prefix = strings.Replace(prefix, sidecarPrefix, originalsPrefix, 1) + log.Debugf("media: replaced sidecar with originals path in related file matching pattern") + } + + // Quote path for glob. if stripSequence { // Strip common name sequences like "copy 2" and escape meta characters. - prefix = regexp.QuoteMeta(m.AbsPrefix(true)) + prefix = regexp.QuoteMeta(prefix) } else { // Use strict file name matching and escape meta characters. - prefix = regexp.QuoteMeta(m.AbsPrefix(false) + ".") + prefix = regexp.QuoteMeta(prefix + ".") } // Find related files. @@ -1085,6 +1097,7 @@ func (m *MediaFile) ColorProfile() string { return m.colorProfile } + start := time.Now() logName := sanitize.Log(m.BaseName()) // Open file. @@ -1109,12 +1122,12 @@ func (m *MediaFile) ColorProfile() string { if iccProfile, err := md.ICCProfile(); err != nil || iccProfile == nil { // Do nothing. } else if profile, err := iccProfile.Description(); err == nil && profile != "" { - log.Debugf("media: %s has color profile %s", logName, sanitize.Log(profile)) + log.Debugf("media: %s has color profile %s [%s]", logName, sanitize.Log(profile), time.Since(start)) m.colorProfile = profile return m.colorProfile } - log.Tracef("media: %s has no color profile", logName) + log.Tracef("media: %s has no color profile [%s]", logName, time.Since(start)) m.noColorProfile = true return "" } diff --git a/internal/photoprism/related.go b/internal/photoprism/related.go index e6f96883b..c4faf6fad 100644 --- a/internal/photoprism/related.go +++ b/internal/photoprism/related.go @@ -2,9 +2,11 @@ package photoprism import ( "strings" + + "github.com/photoprism/photoprism/pkg/sanitize" ) -// List of related files for importing and indexing. +// RelatedFiles represents a list of related files to be indexed or imported. type RelatedFiles struct { Files MediaFiles Main *MediaFile @@ -40,3 +42,30 @@ func (m RelatedFiles) String() string { func (m RelatedFiles) Len() int { return len(m.Files) } + +// Count returns the number of files without the main file. +func (m RelatedFiles) Count() int { + if l := m.Len(); l < 1 { + return l + } else { + return l - 1 + } +} + +// MainFileType returns the main file type as string. +func (m RelatedFiles) MainFileType() string { + if m.Main == nil { + return "" + } + + return string(m.Main.FileType()) +} + +// MainLogName returns the main file name for logging. +func (m RelatedFiles) MainLogName() string { + if m.Main == nil { + return "" + } + + return sanitize.Log(m.Main.RelName(Config().OriginalsPath())) +} diff --git a/internal/photoprism/related_test.go b/internal/photoprism/related_test.go index 58299e609..e3089c03f 100644 --- a/internal/photoprism/related_test.go +++ b/internal/photoprism/related_test.go @@ -3,8 +3,11 @@ package photoprism import ( "testing" - "github.com/photoprism/photoprism/internal/config" "github.com/stretchr/testify/assert" + + "github.com/photoprism/photoprism/internal/config" + + "github.com/photoprism/photoprism/pkg/fs" ) func TestRelatedFiles_ContainsJpeg(t *testing.T) { @@ -93,3 +96,126 @@ func TestRelatedFiles_Len(t *testing.T) { assert.Equal(t, 2, relatedFiles.Len()) }) } + +func TestRelatedFiles_Count(t *testing.T) { + conf := config.TestConfig() + t.Run("NoMainFile", func(t *testing.T) { + relatedFiles := RelatedFiles{ + Files: MediaFiles{}, + Main: nil, + } + assert.Equal(t, 0, relatedFiles.Count()) + }) + t.Run("None", func(t *testing.T) { + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg") + if err != nil { + t.Fatal(err) + } + relatedFiles := RelatedFiles{ + Files: MediaFiles{}, + Main: mediaFile, + } + assert.Equal(t, 0, relatedFiles.Count()) + }) + t.Run("One", func(t *testing.T) { + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg") + if err != nil { + t.Fatal(err) + } + mediaFile2, err2 := NewMediaFile(conf.ExamplesPath() + "/Screenshot 2019-05-21 at 10.45.52.png") + if err2 != nil { + t.Fatal(err2) + } + mediaFile3, err3 := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic") + if err3 != nil { + t.Fatal(err3) + } + relatedFiles := RelatedFiles{ + Files: MediaFiles{mediaFile, mediaFile2}, + Main: mediaFile3, + } + assert.Equal(t, 1, relatedFiles.Count()) + }) +} + +func TestRelatedFiles_MainFileType(t *testing.T) { + conf := config.TestConfig() + t.Run("None", func(t *testing.T) { + relatedFiles := RelatedFiles{ + Files: MediaFiles{}, + Main: nil, + } + assert.Equal(t, "", relatedFiles.MainFileType()) + }) + t.Run("Jpeg", func(t *testing.T) { + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg") + if err != nil { + t.Fatal(err) + } + relatedFiles := RelatedFiles{ + Files: MediaFiles{}, + Main: mediaFile, + } + assert.Equal(t, string(fs.FormatJpeg), relatedFiles.MainFileType()) + }) + t.Run("Heif", func(t *testing.T) { + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg") + if err != nil { + t.Fatal(err) + } + mediaFile2, err2 := NewMediaFile(conf.ExamplesPath() + "/Screenshot 2019-05-21 at 10.45.52.png") + if err2 != nil { + t.Fatal(err2) + } + mediaFile3, err3 := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic") + if err3 != nil { + t.Fatal(err3) + } + relatedFiles := RelatedFiles{ + Files: MediaFiles{mediaFile, mediaFile2}, + Main: mediaFile3, + } + assert.Equal(t, string(fs.FormatHEIF), relatedFiles.MainFileType()) + }) +} + +func TestRelatedFiles_MainLogName(t *testing.T) { + conf := config.TestConfig() + t.Run("None", func(t *testing.T) { + relatedFiles := RelatedFiles{ + Files: MediaFiles{}, + Main: nil, + } + assert.Equal(t, "", relatedFiles.MainFileType()) + }) + t.Run("Telegram", func(t *testing.T) { + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg") + if err != nil { + t.Fatal(err) + } + relatedFiles := RelatedFiles{ + Files: MediaFiles{}, + Main: mediaFile, + } + assert.Equal(t, conf.ExamplesPath()+"/telegram_2020-01-30_09-57-18.jpg", relatedFiles.MainLogName()) + }) + t.Run("iPhone7", func(t *testing.T) { + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg") + if err != nil { + t.Fatal(err) + } + mediaFile2, err2 := NewMediaFile(conf.ExamplesPath() + "/Screenshot 2019-05-21 at 10.45.52.png") + if err2 != nil { + t.Fatal(err2) + } + mediaFile3, err3 := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic") + if err3 != nil { + t.Fatal(err3) + } + relatedFiles := RelatedFiles{ + Files: MediaFiles{mediaFile, mediaFile2, mediaFile3}, + Main: mediaFile3, + } + assert.Equal(t, conf.ExamplesPath()+"/iphone_7.heic", relatedFiles.MainLogName()) + }) +} diff --git a/internal/query/files.go b/internal/query/files.go index 1fe099001..7449e7ca9 100644 --- a/internal/query/files.go +++ b/internal/query/files.go @@ -111,7 +111,7 @@ func SetPhotoPrimary(photoUID, fileUID string) error { if fileUID != "" { // Do nothing. - } else if err := Db().Model(entity.File{}).Where("photo_uid = ? AND file_missing = 0 AND file_type = 'jpg'", photoUID).Order("file_width DESC").Limit(1).Pluck("file_uid", &files).Error; err != nil { + } else if err := Db().Model(entity.File{}).Where("photo_uid = ? AND file_missing = 0 AND file_type = 'jpg'", photoUID).Order("file_width DESC, file_hdr DESC").Limit(1).Pluck("file_uid", &files).Error; err != nil { return err } else if len(files) == 0 { return fmt.Errorf("cannot find primary file for %s", photoUID)