Index: Improve indexing and unstacking of related files #1823

This commit also adds initial HDR flag extraction from metadata.
This commit is contained in:
Michael Mayer 2022-01-05 16:37:19 +01:00
parent a1ee2c4d6c
commit dd9d7123d9
21 changed files with 452 additions and 137 deletions

View file

@ -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",

View file

@ -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>

View file

@ -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

View file

@ -62,6 +62,7 @@ export class File extends RestModel {
Orientation: 0,
Projection: "",
AspectRatio: 1.0,
HDR: false,
ColorProfile: "",
MainColor: "",
Colors: "",

View file

@ -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:

View file

@ -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)

View file

@ -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,

View file

@ -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")

View file

@ -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 {

View file

@ -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()
}

View file

@ -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))

View file

@ -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)

View file

@ -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
}
}
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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)
})
}

View file

@ -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 {

View file

@ -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 ""
}

View file

@ -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()))
}

View file

@ -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())
})
}

View file

@ -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)