Metadata: Improve handling of local time values #3780
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
60efc86649
commit
67bd054f7b
6 changed files with 179 additions and 163 deletions
128
frontend/package-lock.json
generated
128
frontend/package-lock.json
generated
|
@ -2710,19 +2710,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz",
|
||||
"integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==",
|
||||
"version": "8.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz",
|
||||
"integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.12",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.12.tgz",
|
||||
"integrity": "sha512-NlGesA1usRNn6ctHCZ21M4/dKPgW9Nn1FypRdIKKgZOKzkVV4T1FlK5mBiLhHBCDmEbdQG0idrcXlbZfksJ+RA==",
|
||||
"version": "0.11.13",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
|
||||
"dependencies": {
|
||||
"@humanwhocodes/object-schema": "^2.0.0",
|
||||
"@humanwhocodes/object-schema": "^2.0.1",
|
||||
"debug": "^4.1.1",
|
||||
"minimatch": "^3.0.5"
|
||||
},
|
||||
|
@ -2743,9 +2743,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/object-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.0.tgz",
|
||||
"integrity": "sha512-9S9QrXY2K0L4AGDcSgTi9vgiCcG8VcBv4Mp7/1hDPYoswIy6Z6KO5blYto82BT8M0MZNRWmCFLpCs3HlpYGGdw=="
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
|
||||
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw=="
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.2.0",
|
||||
|
@ -2990,9 +2990,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||
"version": "19.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.2.tgz",
|
||||
"integrity": "sha512-C2JAk64XUz9v78+bpyTk1zvgjjnDsB8CCjNumyAYdWK2dvdDtILzh1AGBMdS/llX3KaHjGYxAE5wOwfdwq4Pog==",
|
||||
"version": "19.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz",
|
||||
"integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
|
@ -3216,37 +3216,42 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz",
|
||||
"integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw=="
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.5.tgz",
|
||||
"integrity": "sha512-S8Ma+eICI40Y4UotR+iKR729Bma+wERn/xLc+Jz203s5WIW1Sx3qoiONqXGg3Q4vBMa+QHDncULya19ZSJuhog==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.6.tgz",
|
||||
"integrity": "sha512-2JNjemwaNwf+MkkatATVZi7oAH1Hx0B04DdPH3ZoZ8vKC1xZVP7nl4HIsk8XYd3r+/52sqqoz9TWzYc3yE9dqA==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@vue/shared": "3.3.5",
|
||||
"@vue/shared": "3.3.6",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.5.tgz",
|
||||
"integrity": "sha512-dxt6QntN9T/NtnV6Pz+/nmcoo3ULnsYCnRpvEyY73wbk1tzzx7dnwngUN1cXkyGNu9c3UE7llhq/5T54lKwyhQ==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.6.tgz",
|
||||
"integrity": "sha512-1MxXcJYMHiTPexjLAJUkNs/Tw2eDf2tY3a0rL+LfuWyiKN2s6jvSwywH3PWD8bKICjfebX3GWx2Os8jkRDq3Ng==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.3.5",
|
||||
"@vue/shared": "3.3.5"
|
||||
"@vue/compiler-core": "3.3.6",
|
||||
"@vue/shared": "3.3.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.5.tgz",
|
||||
"integrity": "sha512-M6ys4iReSbrF4NTcMCnJiBioCpzXjfkfXwkdziknRyps+pG0DkwpDfQT7zQ0q91/rCR/Ejz64b5H6C4HBhX41w==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.6.tgz",
|
||||
"integrity": "sha512-/Kms6du2h1VrXFreuZmlvQej8B1zenBqIohP0690IUBkJjsFvJxY0crcvVRJ0UhMgSR9dewB+khdR1DfbpArJA==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@vue/compiler-core": "3.3.5",
|
||||
"@vue/compiler-dom": "3.3.5",
|
||||
"@vue/compiler-ssr": "3.3.5",
|
||||
"@vue/reactivity-transform": "3.3.5",
|
||||
"@vue/shared": "3.3.5",
|
||||
"@vue/compiler-core": "3.3.6",
|
||||
"@vue/compiler-dom": "3.3.6",
|
||||
"@vue/compiler-ssr": "3.3.6",
|
||||
"@vue/reactivity-transform": "3.3.6",
|
||||
"@vue/shared": "3.3.6",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.5",
|
||||
"postcss": "^8.4.31",
|
||||
|
@ -3254,12 +3259,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.5.tgz",
|
||||
"integrity": "sha512-v7p2XuEpOcgjd6c49NqOnq3UTJOv5Uo9tirOyGnEadwxTov2O1J3/TUt4SgAAnwA+9gcUyH5c3lIOFsBe+UIyw==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.6.tgz",
|
||||
"integrity": "sha512-QTIHAfDCHhjXlYGkUg5KH7YwYtdUM1vcFl/FxFDlD6d0nXAmnjizka3HITp8DGudzHndv2PjKVS44vqqy0vP4w==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.3.5",
|
||||
"@vue/shared": "3.3.5"
|
||||
"@vue/compiler-dom": "3.3.6",
|
||||
"@vue/shared": "3.3.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/component-compiler-utils": {
|
||||
|
@ -3331,26 +3336,26 @@
|
|||
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
|
||||
},
|
||||
"node_modules/@vue/reactivity-transform": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.5.tgz",
|
||||
"integrity": "sha512-OhpBD1H32pIapRzqy31hWwTFLf9STP+0uk5bVOQWXACTa2Rt/RPhvX4zixbPgMGo6iP+S+tFpZzUdcG8AASn8A==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.6.tgz",
|
||||
"integrity": "sha512-RlJl4dHfeO7EuzU1iJOsrlqWyJfHTkJbvYz/IOJWqu8dlCNWtxWX377WI0VsbAgBizjwD+3ZjdnvSyyFW1YVng==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@vue/compiler-core": "3.3.5",
|
||||
"@vue/shared": "3.3.5",
|
||||
"@vue/compiler-core": "3.3.6",
|
||||
"@vue/shared": "3.3.6",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.5.tgz",
|
||||
"integrity": "sha512-oNJN1rCtkqm1cIxU1BuZVEVRWIp4DhaxXucEzzZ/iDKHP71ZxhkBPNK+URySiECH6aiOZzC60PS2bd6JFznvNA=="
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.6.tgz",
|
||||
"integrity": "sha512-Xno5pEqg8SVhomD0kTSmfh30ZEmV/+jZtyh39q6QflrjdJCXah5lrnOLi9KB6a5k5aAHXMXjoMnxlzUkCNfWLQ=="
|
||||
},
|
||||
"node_modules/@vvo/tzdb": {
|
||||
"version": "6.108.0",
|
||||
"resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.108.0.tgz",
|
||||
"integrity": "sha512-/UI2yKYNlcPVsVajMNcLfcsZgD+TtmE9hsN+3JTrk8N4/Kwlr35SqMOZuSU7lwWG+PvWmWKs51f2SMM0JGWxww=="
|
||||
"version": "6.109.0",
|
||||
"resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.109.0.tgz",
|
||||
"integrity": "sha512-HFE2m2YIiW0POGepiHAPYlqzv9YZxc96faxVH0UOen4Djvl+l3fSVeeTgQRCOCy+aKLtqALthVrVgt8BOlWkmg=="
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.11.6",
|
||||
|
@ -5809,9 +5814,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.561",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.561.tgz",
|
||||
"integrity": "sha512-eS5t4ulWOBfVHdq9SW2dxEaFarj1lPjvJ8PaYMOjY0DecBaj/t4ARziL2IPpDr4atyWwjLFGQ2vo/VCgQFezVQ=="
|
||||
"version": "1.4.563",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.563.tgz",
|
||||
"integrity": "sha512-dg5gj5qOgfZNkPNeyKBZQAQitIQ/xwfIDmEQJHCbXaD9ebTZxwJXUsDYcBlAvZGZLi+/354l35J1wkmP6CqYaw=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
|
@ -6022,17 +6027,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz",
|
||||
"integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
|
||||
"version": "8.52.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz",
|
||||
"integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.2",
|
||||
"@eslint/js": "8.51.0",
|
||||
"@humanwhocodes/config-array": "^0.11.11",
|
||||
"@eslint/js": "8.52.0",
|
||||
"@humanwhocodes/config-array": "^0.11.13",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"@ungap/structured-clone": "^1.2.0",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
|
@ -7460,19 +7466,19 @@
|
|||
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
|
||||
},
|
||||
"node_modules/flow-parser": {
|
||||
"version": "0.219.2",
|
||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.219.2.tgz",
|
||||
"integrity": "sha512-OqzmNECXX85x/5L/OP9TfHErdDoSUoKR4y1sTTy/A5K2arwl7s5EmX0XTkkcJPlCAHYkElWj5Se+ZwNN/6ry2Q==",
|
||||
"version": "0.219.3",
|
||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.219.3.tgz",
|
||||
"integrity": "sha512-dyPC0+TwAcBMQ1IZhSpj91mxZ31AI9FJ3q/ZMt8kdKaITnDCGmyUyWOwUfAKBVLrUTkdaTfpla0muhwOGY+dXw==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/flow-remove-types": {
|
||||
"version": "2.219.2",
|
||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.219.2.tgz",
|
||||
"integrity": "sha512-g0BFqtf882YOntBvMSXXz7qTEsIKuLBefzk0mLy3PkRDDty1jYmxAorDg9xY7ydWNyONohNaeNVg4x33wGpWlw==",
|
||||
"version": "2.219.3",
|
||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.219.3.tgz",
|
||||
"integrity": "sha512-xYAJIcShkcYALDbMbGGDqOgZTEdH56QbF6M6pOqU1Nww9m1U7y1PJpBXkQIlOolvqZyaEy/gDr0gNweOOspJyg==",
|
||||
"dependencies": {
|
||||
"flow-parser": "^0.219.2",
|
||||
"flow-parser": "^0.219.3",
|
||||
"pirates": "^3.0.2",
|
||||
"vlq": "^0.2.1"
|
||||
},
|
||||
|
|
|
@ -12,8 +12,6 @@ import {
|
|||
} from "model/photo";
|
||||
|
||||
export const UtcOffsets = [
|
||||
{ ID: "UTC-14", Name: "UTC-14:00" },
|
||||
{ ID: "UTC-13", Name: "UTC-13:00" },
|
||||
{ ID: "UTC-12", Name: "UTC-12:00" },
|
||||
{ ID: "UTC-11", Name: "UTC-11:00" },
|
||||
{ ID: "UTC-10", Name: "UTC-10:00" },
|
||||
|
|
|
@ -9,8 +9,8 @@ describe("options/options", () => {
|
|||
const timezones = options.TimeZones();
|
||||
assert.equal(timezones[0].ID, "");
|
||||
assert.equal(timezones[0].Name, "Local Time");
|
||||
assert.equal(timezones[1].ID, "UTC-14");
|
||||
assert.equal(timezones[1].Name, "UTC-14:00");
|
||||
assert.equal(timezones[1].ID, "UTC-12");
|
||||
assert.equal(timezones[1].Name, "UTC-12:00");
|
||||
});
|
||||
|
||||
it("should get days", () => {
|
||||
|
|
|
@ -243,49 +243,49 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
|
|||
|
||||
// Set time zone and calculate UTC time.
|
||||
if data.Lat != 0 && data.Lng != 0 {
|
||||
zones, err := tz.GetZone(tz.Point{
|
||||
zones, zoneErr := tz.GetZone(tz.Point{
|
||||
Lat: float64(data.Lat),
|
||||
Lon: float64(data.Lng),
|
||||
})
|
||||
|
||||
if err == nil && len(zones) > 0 {
|
||||
if zoneErr == nil && len(zones) > 0 {
|
||||
data.TimeZone = zones[0]
|
||||
}
|
||||
|
||||
if loc := txt.TimeZone(data.TimeZone); loc == nil {
|
||||
log.Warnf("metadata: %s has invalid time zone %s (exiftool)", logName)
|
||||
} else if !data.TakenAtLocal.IsZero() {
|
||||
if tl, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), loc); err == nil {
|
||||
if tl, parseErr := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), loc); parseErr == nil {
|
||||
if localUtc, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), time.UTC); err == nil {
|
||||
data.TakenAtLocal = localUtc
|
||||
}
|
||||
|
||||
data.TakenAt = tl.Truncate(time.Second).UTC()
|
||||
} else {
|
||||
log.Errorf("metadata: %s (exiftool)", err.Error()) // this should never happen
|
||||
log.Errorf("metadata: %s (exiftool)", parseErr.Error()) // this should never happen
|
||||
}
|
||||
} else if !data.TakenAt.IsZero() {
|
||||
if localUtc, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAt.In(loc).Format("2006:01:02 15:04:05"), time.UTC); err == nil {
|
||||
if localUtc, parseErr := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAt.In(loc).Format("2006:01:02 15:04:05"), time.UTC); parseErr == nil {
|
||||
data.TakenAtLocal = localUtc
|
||||
data.TakenAt = data.TakenAt.UTC()
|
||||
} else {
|
||||
log.Errorf("metadata: %s (exiftool)", err.Error()) // this should never happen
|
||||
log.Errorf("metadata: %s (exiftool)", parseErr.Error()) // this should never happen
|
||||
}
|
||||
}
|
||||
} else if hasTimeOffset {
|
||||
if localUtc, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), time.UTC); err == nil {
|
||||
if localUtc, parseErr := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), time.UTC); parseErr == nil {
|
||||
data.TakenAtLocal = localUtc.Truncate(time.Second).UTC()
|
||||
}
|
||||
|
||||
data.TakenAt = data.TakenAt.Truncate(time.Second).UTC()
|
||||
}
|
||||
|
||||
// Default to UTC offset time zone?
|
||||
if data.TimeZone != "" && data.TimeZone != "UTC" || data.TakenAtLocal.IsZero() || data.TakenAt.IsZero() {
|
||||
// Set UTC offset as time zone?
|
||||
if data.TimeZone != "" && data.TimeZone != "UTC" || data.TakenAt.IsZero() {
|
||||
// Don't change existing time zone.
|
||||
} else if z := txt.UtcOffset(data.TakenAtLocal, data.TakenAt, data.TimeOffset); z != "" {
|
||||
data.TimeZone = z
|
||||
log.Infof("metadata: %s has time offset %s (exiftool)", logName, clean.Log(data.TimeZone))
|
||||
} else if utcOffset := txt.UtcOffset(data.TakenAtLocal, data.TakenAt, data.TimeOffset); utcOffset != "" {
|
||||
data.TimeZone = utcOffset
|
||||
log.Infof("metadata: %s has time offset %s (exiftool)", logName, clean.Log(utcOffset))
|
||||
} else if data.TimeOffset != "" {
|
||||
log.Infof("metadata: %s has invalid time offset %s (exiftool)", logName, clean.Log(data.TimeOffset))
|
||||
}
|
||||
|
|
|
@ -9,30 +9,19 @@ import (
|
|||
|
||||
// TimeZone returns a time zone for the given UTC offset string.
|
||||
func TimeZone(offset string) *time.Location {
|
||||
if IsUtcOffset(offset) {
|
||||
sec := TimeOffset(offset)
|
||||
if sec == 0 {
|
||||
return time.UTC
|
||||
if offset == "" {
|
||||
// Local time.
|
||||
} else if offset == "UTC" || offset == "Z" {
|
||||
return time.UTC
|
||||
} else if seconds, err := TimeOffset(offset); err == nil {
|
||||
if h := seconds / 3600; h > 0 || h < 0 {
|
||||
return time.FixedZone(fmt.Sprintf("UTC%+d", h), seconds)
|
||||
}
|
||||
return time.FixedZone(fmt.Sprintf("UTC%+d", sec/3600), sec)
|
||||
} else if location, err := time.LoadLocation(offset); err == nil {
|
||||
return location
|
||||
} else if zone, zoneErr := time.LoadLocation(offset); zoneErr == nil {
|
||||
return zone
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsUtcOffset checks if the string is a valid UTC time offset.
|
||||
func IsUtcOffset(s string) bool {
|
||||
if l := len(s); l < 3 || l > 6 {
|
||||
return false
|
||||
} else if s == "UTC" {
|
||||
return true
|
||||
} else if !strings.HasPrefix(s, "UTC") {
|
||||
return false
|
||||
}
|
||||
|
||||
return TimeOffset(s) != 0
|
||||
return time.FixedZone("", 0)
|
||||
}
|
||||
|
||||
// NormalizeUtcOffset returns a normalized UTC time offset string.
|
||||
|
@ -68,7 +57,7 @@ func NormalizeUtcOffset(s string) string {
|
|||
return "UTC-2"
|
||||
case "-1", "-01", "-01:00", "UTC-1", "UTC-01:00":
|
||||
return "UTC-1"
|
||||
case "+0", "+00", "00:00", "+00:00", "-00:00", "Z", "Z00:00", "UTC", "UTC+0", "UTC-0", "UTC+00:00", "UTC-00:00":
|
||||
case "Z", "UTC", "UTC+0", "UTC-0", "UTC+00:00", "UTC-00:00":
|
||||
return time.UTC.String()
|
||||
case "01:00", "+1", "+01", "+01:00", "UTC+1", "UTC+01:00":
|
||||
return "UTC+1"
|
||||
|
@ -117,66 +106,69 @@ func UtcOffset(local, utc time.Time, offset string) string {
|
|||
}
|
||||
|
||||
// Check if time difference is within expected range (hours).
|
||||
if d < -12 || d > 12 {
|
||||
if h := int(d); h == 0 || h < -12 || h > 12 {
|
||||
return ""
|
||||
} else if d == 0 {
|
||||
return time.UTC.String()
|
||||
} else {
|
||||
return fmt.Sprintf("UTC%+d", h)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("UTC%+d", int(d))
|
||||
}
|
||||
|
||||
func TimeOffset(s string) (seconds int) {
|
||||
switch s {
|
||||
// TimeOffset returns the UTC time offset in seconds or an error if it is invalid.
|
||||
func TimeOffset(utcOffset string) (seconds int, err error) {
|
||||
switch utcOffset {
|
||||
case "-12", "-12:00", "UTC-12", "UTC-12:00":
|
||||
return -12 * 3600
|
||||
seconds = -12 * 3600
|
||||
case "-11", "-11:00", "UTC-11", "UTC-11:00":
|
||||
return -11 * 3600
|
||||
seconds = -11 * 3600
|
||||
case "-10", "-10:00", "UTC-10", "UTC-10:00":
|
||||
return -10 * 3600
|
||||
seconds = -10 * 3600
|
||||
case "-9", "-09", "-09:00", "UTC-9", "UTC-09:00":
|
||||
return -9 * 3600
|
||||
seconds = -9 * 3600
|
||||
case "-8", "-08", "-08:00", "UTC-8", "UTC-08:00":
|
||||
return -8 * 3600
|
||||
seconds = -8 * 3600
|
||||
case "-7", "-07", "-07:00", "UTC-7", "UTC-07:00":
|
||||
return -7 * 3600
|
||||
seconds = -7 * 3600
|
||||
case "-6", "-06", "-06:00", "UTC-6", "UTC-06:00":
|
||||
return -6 * 3600
|
||||
seconds = -6 * 3600
|
||||
case "-5", "-05", "-05:00", "UTC-5", "UTC-05:00":
|
||||
return -5 * 3600
|
||||
seconds = -5 * 3600
|
||||
case "-4", "-04", "-04:00", "UTC-4", "UTC-04:00":
|
||||
return -4 * 3600
|
||||
seconds = -4 * 3600
|
||||
case "-3", "-03", "-03:00", "UTC-3", "UTC-03:00":
|
||||
return -3 * 3600
|
||||
seconds = -3 * 3600
|
||||
case "-2", "-02", "-02:00", "UTC-2", "UTC-02:00":
|
||||
return -2 * 3600
|
||||
seconds = -2 * 3600
|
||||
case "-1", "-01", "-01:00", "UTC-1", "UTC-01:00":
|
||||
return -1 * 3600
|
||||
seconds = -1 * 3600
|
||||
case "01:00", "+1", "+01", "+01:00", "UTC+1", "UTC+01:00":
|
||||
return 1 * 3600
|
||||
seconds = 1 * 3600
|
||||
case "02:00", "+2", "+02", "+02:00", "UTC+2", "UTC+02:00":
|
||||
return 2 * 3600
|
||||
seconds = 2 * 3600
|
||||
case "03:00", "+3", "+03", "+03:00", "UTC+3", "UTC+03:00":
|
||||
return 3 * 3600
|
||||
seconds = 3 * 3600
|
||||
case "04:00", "+4", "+04", "+04:00", "UTC+4", "UTC+04:00":
|
||||
return 4 * 3600
|
||||
seconds = 4 * 3600
|
||||
case "05:00", "+5", "+05", "+05:00", "UTC+5", "UTC+05:00":
|
||||
return 5 * 3600
|
||||
seconds = 5 * 3600
|
||||
case "06:00", "+6", "+06", "+06:00", "UTC+6", "UTC+06:00":
|
||||
return 6 * 3600
|
||||
seconds = 6 * 3600
|
||||
case "07:00", "+7", "+07", "+07:00", "UTC+7", "UTC+07:00":
|
||||
return 7 * 3600
|
||||
seconds = 7 * 3600
|
||||
case "08:00", "+8", "+08", "+08:00", "UTC+8", "UTC+08:00":
|
||||
return 8 * 3600
|
||||
seconds = 8 * 3600
|
||||
case "09:00", "+9", "+09", "+09:00", "UTC+9", "UTC+09:00":
|
||||
return 9 * 3600
|
||||
seconds = 9 * 3600
|
||||
case "10:00", "+10", "+10:00", "UTC+10", "UTC+10:00":
|
||||
return 10 * 3600
|
||||
seconds = 10 * 3600
|
||||
case "11:00", "+11", "+11:00", "UTC+11", "UTC+11:00":
|
||||
return 11 * 3600
|
||||
seconds = 11 * 3600
|
||||
case "12:00", "+12", "+12:00", "UTC+12", "UTC+12:00":
|
||||
return 12 * 3600
|
||||
seconds = 12 * 3600
|
||||
case "Z", "UTC", "UTC+0", "UTC-0", "UTC+00:00", "UTC-00:00":
|
||||
seconds = 0
|
||||
default:
|
||||
return 0
|
||||
return 0, fmt.Errorf("invalid UTC offset")
|
||||
}
|
||||
|
||||
return seconds, nil
|
||||
}
|
||||
|
|
|
@ -10,6 +10,14 @@ import (
|
|||
func TestTimeZone(t *testing.T) {
|
||||
t.Run("UTC", func(t *testing.T) {
|
||||
assert.Equal(t, time.UTC.String(), TimeZone(time.UTC.String()).String())
|
||||
assert.Equal(t, time.UTC.String(), TimeZone("Z").String())
|
||||
assert.Equal(t, time.UTC.String(), TimeZone("UTC").String())
|
||||
})
|
||||
t.Run("LocalTime", func(t *testing.T) {
|
||||
assert.Equal(t, "", TimeZone("").String())
|
||||
assert.Equal(t, "", TimeZone("0").String())
|
||||
assert.Equal(t, "", TimeZone("UTC+0").String())
|
||||
assert.Equal(t, "", TimeZone("UTC+00:00").String())
|
||||
})
|
||||
t.Run("UTC+2", func(t *testing.T) {
|
||||
local, err := time.Parse("2006-01-02 15:04:05 Z07:00", "2023-10-02 13:20:17 +00:00")
|
||||
|
@ -34,24 +42,6 @@ func TestTimeZone(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestIsUtcOffset(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
assert.Equal(t, true, IsUtcOffset("UTC-2"))
|
||||
assert.Equal(t, true, IsUtcOffset("UTC"))
|
||||
assert.Equal(t, true, IsUtcOffset("UTC+1"))
|
||||
assert.Equal(t, true, IsUtcOffset("UTC+2"))
|
||||
assert.Equal(t, true, IsUtcOffset("UTC+12"))
|
||||
})
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
assert.Equal(t, false, IsUtcOffset("UTC-15"))
|
||||
assert.Equal(t, false, IsUtcOffset("UTC-14"))
|
||||
assert.Equal(t, false, IsUtcOffset("UTC--2"))
|
||||
assert.Equal(t, false, IsUtcOffset("UTC1"))
|
||||
assert.Equal(t, false, IsUtcOffset("UTC13"))
|
||||
assert.Equal(t, false, IsUtcOffset("UTC+13"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeUtcOffset(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
assert.Equal(t, "UTC-2", NormalizeUtcOffset("UTC-2"))
|
||||
|
@ -112,8 +102,8 @@ func TestUtcOffset(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "UTC", UtcOffset(local, utc, "00:00"))
|
||||
assert.Equal(t, "UTC", UtcOffset(local, utc, "+00:00"))
|
||||
assert.Equal(t, "", UtcOffset(local, utc, "00:00"))
|
||||
assert.Equal(t, "", UtcOffset(local, utc, "+00:00"))
|
||||
assert.Equal(t, "UTC", UtcOffset(local, utc, "Z"))
|
||||
})
|
||||
t.Run("UTC+2", func(t *testing.T) {
|
||||
|
@ -216,19 +206,49 @@ func TestUtcOffset(t *testing.T) {
|
|||
|
||||
func TestTimeOffset(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
assert.Equal(t, -2*3600, TimeOffset("UTC-2"))
|
||||
assert.Equal(t, 0, TimeOffset("UTC"))
|
||||
assert.Equal(t, 3600, TimeOffset("UTC+1"))
|
||||
assert.Equal(t, 2*3600, TimeOffset("UTC+2"))
|
||||
assert.Equal(t, 12*3600, TimeOffset("UTC+12"))
|
||||
sec, err := TimeOffset("UTC-2")
|
||||
assert.Equal(t, -2*3600, sec)
|
||||
assert.NoError(t, err)
|
||||
|
||||
sec, err = TimeOffset("UTC")
|
||||
assert.Equal(t, 0, sec)
|
||||
assert.NoError(t, err)
|
||||
|
||||
sec, err = TimeOffset("UTC+1")
|
||||
assert.Equal(t, 3600, sec)
|
||||
assert.NoError(t, err)
|
||||
|
||||
sec, err = TimeOffset("UTC+2")
|
||||
assert.Equal(t, 2*3600, sec)
|
||||
assert.NoError(t, err)
|
||||
|
||||
sec, err = TimeOffset("UTC+12")
|
||||
assert.Equal(t, 12*3600, sec)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
assert.Equal(t, 0, TimeOffset("UTC-15"))
|
||||
assert.Equal(t, 0, TimeOffset("UTC-14"))
|
||||
assert.Equal(t, 0, TimeOffset("UTC--2"))
|
||||
assert.Equal(t, 0, TimeOffset("UTC0"))
|
||||
assert.Equal(t, 0, TimeOffset("UTC1"))
|
||||
assert.Equal(t, 0, TimeOffset("UTC13"))
|
||||
assert.Equal(t, 0, TimeOffset("UTC+13"))
|
||||
sec, err := TimeOffset("UTC-15")
|
||||
assert.Equal(t, 0, sec)
|
||||
assert.Error(t, err)
|
||||
|
||||
sec, err = TimeOffset("UTC--2")
|
||||
assert.Equal(t, 0, sec)
|
||||
assert.Error(t, err)
|
||||
|
||||
sec, err = TimeOffset("UTC0")
|
||||
assert.Equal(t, 0, sec)
|
||||
assert.Error(t, err)
|
||||
|
||||
sec, err = TimeOffset("UTC1")
|
||||
assert.Equal(t, 0, sec)
|
||||
assert.Error(t, err)
|
||||
|
||||
sec, err = TimeOffset("UTC13")
|
||||
assert.Equal(t, 0, sec)
|
||||
assert.Error(t, err)
|
||||
|
||||
sec, err = TimeOffset("UTC+13")
|
||||
assert.Equal(t, 0, sec)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue