Index: Improve indexing and unstacking of related files #1823
This commit also adds initial HDR flag extraction from metadata.
This commit is contained in:
parent
a1ee2c4d6c
commit
dd9d7123d9
125
frontend/package-lock.json
generated
125
frontend/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -154,6 +154,14 @@
|
|||
<translate>{{ file.Orientation }}</translate>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="file.HDR">
|
||||
<td>
|
||||
<translate>HDR</translate>
|
||||
</td>
|
||||
<td>
|
||||
<translate>Yes</translate>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="file.ColorProfile">
|
||||
<td>
|
||||
<translate>Color Profile</translate>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -62,6 +62,7 @@ export class File extends RestModel {
|
|||
Orientation: 0,
|
||||
Projection: "",
|
||||
AspectRatio: 1.0,
|
||||
HDR: false,
|
||||
ColorProfile: "",
|
||||
MainColor: "",
|
||||
Colors: "",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ""
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue