Implement time zone support for "TakenAt"
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
a2bf11c96f
commit
0f47c84138
15
frontend/package-lock.json
generated
15
frontend/package-lock.json
generated
|
@ -6849,6 +6849,11 @@
|
|||
"yallist": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"luxon": {
|
||||
"version": "1.17.3",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.17.3.tgz",
|
||||
"integrity": "sha512-gG894ALYdWB3izyTGDypF6ffablXs5oleJLRNboL/xxYa0Dm5apXihaCnHecMXNqQ3vW1qjCCZ+fXAX5Azgx3Q=="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
|
@ -11157,12 +11162,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"vue-moment": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-moment/-/vue-moment-4.0.0.tgz",
|
||||
"integrity": "sha512-lNkEPuA3i3A4q4TDSwOXoRF4Y2vHHdaTOSvpPyGgxoFQP8n4sUh6jU5aJj3FIMlXo5UaHLPIz5hvvpvYx9Wj0w==",
|
||||
"vue-luxon": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-luxon/-/vue-luxon-0.7.0.tgz",
|
||||
"integrity": "sha512-EycNzfRqTi97kj+1UyjadezNCXgQHSgE21yfAxCKQqylAl9AOZi2QSe6OeteH3BivZv/w7CdJfFNEqttGpVQ4Q==",
|
||||
"requires": {
|
||||
"moment": "^2.11.1"
|
||||
"luxon": "^1.17.2"
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
|
|
|
@ -65,10 +65,10 @@
|
|||
"karma-verbose-reporter": "^0.0.6",
|
||||
"karma-webpack": "^4.0.2",
|
||||
"leaflet": "^1.5.1",
|
||||
"luxon": "^1.17.3",
|
||||
"material-design-icons-iconfont": "^5.0.1",
|
||||
"mini-css-extract-plugin": "^0.7.0",
|
||||
"mocha": "^6.2.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.26",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"ora": "^3.4.0",
|
||||
|
@ -96,7 +96,7 @@
|
|||
"vue-fullscreen": "^2.1.5",
|
||||
"vue-infinite-scroll": "^2.0.2",
|
||||
"vue-loader": "^14.2.4",
|
||||
"vue-moment": "^4.0.0",
|
||||
"vue-luxon": "^0.7.0",
|
||||
"vue-router": "^3.1.3",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
|
|
|
@ -13,10 +13,11 @@ import Alert from "common/alert";
|
|||
import Viewer from "common/viewer";
|
||||
import Session from "common/session";
|
||||
import Event from "pubsub-js";
|
||||
import VueMoment from "vue-moment";
|
||||
import VueLuxon from "vue-luxon";
|
||||
import VueInfiniteScroll from "vue-infinite-scroll";
|
||||
import VueFullscreen from "vue-fullscreen";
|
||||
import VueFilters from "vue2-filters";
|
||||
import { Settings } from "luxon";
|
||||
|
||||
// Initialize helpers
|
||||
const session = new Session(window.localStorage);
|
||||
|
@ -48,8 +49,10 @@ Vue.use(Vuetify, {
|
|||
},
|
||||
});
|
||||
|
||||
Settings.defaultLocale = "en";
|
||||
|
||||
// Register other VueJS plugins
|
||||
Vue.use(VueMoment);
|
||||
Vue.use(VueLuxon);
|
||||
Vue.use(VueInfiniteScroll);
|
||||
Vue.use(VueFullscreen);
|
||||
Vue.use(VueFilters);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import axios from "axios";
|
||||
import Event from "pubsub-js";
|
||||
import "@babel/polyfill";
|
||||
import "@babel/polyfill/noConflict";
|
||||
|
||||
const Api = axios.create({
|
||||
baseURL: "/api/v1",
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</v-btn>
|
||||
</td>
|
||||
<td @click="openPhoto(props.index)" class="p-pointer">{{ props.item.PhotoTitle }}</td>
|
||||
<td>{{ props.item.TakenAt | moment('DD/MM/YYYY hh:mm:ss') }}</td>
|
||||
<td>{{ props.item.TakenAt | luxon:format('dd/MM/yyyy hh:mm:ss') }}</td>
|
||||
<td @click="openLocation(props.index)" class="p-pointer">{{ props.item.LocCountry }}</td>
|
||||
<td>{{ props.item.CameraMake }} {{ props.item.CameraModel }}</td>
|
||||
<td><v-btn icon small flat :ripple="false"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Abstract from "model/abstract";
|
||||
import Api from "common/api";
|
||||
import moment from "moment";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
class Album extends Abstract {
|
||||
getEntityName() {
|
||||
|
@ -44,7 +44,7 @@ class Album extends Abstract {
|
|||
}
|
||||
|
||||
getDateString() {
|
||||
return moment(this.CreatedAt).format("LLL");
|
||||
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
|
||||
}
|
||||
|
||||
toggleLike() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Abstract from "model/abstract";
|
||||
import Api from "common/api";
|
||||
import moment from "moment";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
class Label extends Abstract {
|
||||
getEntityName() {
|
||||
|
@ -44,7 +44,7 @@ class Label extends Abstract {
|
|||
}
|
||||
|
||||
getDateString() {
|
||||
return moment(this.CreatedAt).format("LLL");
|
||||
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
|
||||
}
|
||||
|
||||
toggleLike() {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Abstract from "model/abstract";
|
||||
import Api from "common/api";
|
||||
import truncate from "truncate";
|
||||
import moment from "moment";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
class Photo extends Abstract {
|
||||
getEntityName() {
|
||||
|
@ -91,7 +91,13 @@ class Photo extends Abstract {
|
|||
}
|
||||
|
||||
getDateString() {
|
||||
return moment(this.TakenAt).format("LLL");
|
||||
if(this.TimeZone) {
|
||||
return DateTime.fromISO(this.TakenAt).setZone(this.TimeZone).toLocaleString(DateTime.DATETIME_FULL);
|
||||
} else if(this.TakenAt) {
|
||||
return DateTime.fromISO(this.TakenAt).toLocaleString(DateTime.DATE_HUGE);
|
||||
} else {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
hasLocation() {
|
||||
|
|
|
@ -171,7 +171,7 @@
|
|||
></v-checkbox>
|
||||
</td>
|
||||
<td>{{ props.item.PhotoTitle }}</td>
|
||||
<td>{{ props.item.TakenAt | moment('DD/MM/YYYY hh:mm:ss') }}</td>
|
||||
<td>{{ props.item.TakenAt | luxon:format('dd/MM/yyyy hh:mm:ss') }}</td>
|
||||
<td>{{ props.item.LocCity }}</td>
|
||||
<td>{{ props.item.LocCountry }}</td>
|
||||
<td>{{ props.item.CameraModel }}</td>
|
||||
|
@ -240,7 +240,7 @@
|
|||
truncate(80) }}</h3>
|
||||
<div class="caption">
|
||||
<v-icon size="14">date_range</v-icon>
|
||||
{{ photo.TakenAt | moment('DD/MM/YYYY hh:mm:ss') }}
|
||||
{{ photo.TakenAt | luxon:format('dd/MM/yyyy hh:mm:ss') }}
|
||||
<br/>
|
||||
<v-icon size="14">photo_camera</v-icon>
|
||||
{{ photo.getCamera() }}
|
||||
|
|
|
@ -175,4 +175,4 @@ describe("common/clipboard", () => {
|
|||
assert.equal(clipboard.selection, "");
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -48,18 +48,17 @@ describe("model/album", () => {
|
|||
});
|
||||
|
||||
it("should get thumbnail sizes", () => {
|
||||
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019"};
|
||||
const values = {id: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", CreatedAt: "2012-07-08T14:45:39Z"};
|
||||
const album = new Album(values);
|
||||
const result = album.getThumbnailSizes();
|
||||
assert.equal(result, "(min-width: 2560px) 3840px, (min-width: 1920px) 2560px, (min-width: 1280px) 1920px, (min-width: 720px) 1280px, 720px");
|
||||
});
|
||||
|
||||
it("should get date string", () => {
|
||||
const t = "2012-07-08 14:45:39";
|
||||
const values = {ID: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", CreatedAt: t};
|
||||
const values = {ID: 5, AlbumName: "Christmas 2019", AlbumSlug: "christmas-2019", CreatedAt: "2012-07-08T14:45:39Z"};
|
||||
const album = new Album(values);
|
||||
const result = album.getDateString();
|
||||
assert.equal(result, "July 8, 2012 2:45 PM");
|
||||
assert.equal(result, "Jul 8, 2012, 2:45 PM");
|
||||
});
|
||||
|
||||
it("should get model name", () => {
|
||||
|
@ -97,4 +96,4 @@ describe("model/album", () => {
|
|||
album.toggleLike();
|
||||
assert.equal(album.AlbumFavorite, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -55,11 +55,10 @@ describe("model/label", () => {
|
|||
});
|
||||
|
||||
it("should get date string", () => {
|
||||
const t = "2012-07-08 14:45:39";
|
||||
const values = {ID: 5, LabelName: "Black Cat", LabelSlug: "black-cat", CreatedAt: t};
|
||||
const values = {ID: 5, LabelName: "Black Cat", LabelSlug: "black-cat", CreatedAt: "2012-07-08T14:45:39Z"};
|
||||
const label = new Label(values);
|
||||
const result = label.getDateString();
|
||||
assert.equal(result, "July 8, 2012 2:45 PM");
|
||||
assert.equal(result, "Jul 8, 2012, 2:45 PM");
|
||||
});
|
||||
|
||||
it("should get model name", () => {
|
||||
|
@ -97,4 +96,4 @@ describe("model/label", () => {
|
|||
label.toggleLike();
|
||||
assert.equal(label.LabelFavorite, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -114,11 +114,10 @@ describe("model/photo", () => {
|
|||
});
|
||||
|
||||
it("should get date string", () => {
|
||||
const t = "2012-07-08 14:45:39";
|
||||
const values = {ID: 5, PhotoTitle: "Crazy Cat", TakenAt: t};
|
||||
const values = {ID: 5, PhotoTitle: "Crazy Cat", TakenAt: "2012-07-08T14:45:39Z", TimeZone: "UTC"};
|
||||
const photo = new Photo(values);
|
||||
const result = photo.getDateString();
|
||||
assert.equal(result, "July 8, 2012 2:45 PM");
|
||||
assert.equal(result, "July 8, 2012, 2:45 PM UTC");
|
||||
});
|
||||
|
||||
it("should test whether photo has location", () => {
|
||||
|
@ -234,4 +233,4 @@ describe("model/photo", () => {
|
|||
assert.equal(photo.PhotoFavorite, true);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
1
go.mod
1
go.mod
|
@ -65,6 +65,7 @@ require (
|
|||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
|
||||
gopkg.in/ugjka/go-tz.v2 v2.0.8
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
)
|
||||
|
||||
|
|
4
go.sum
4
go.sum
|
@ -170,6 +170,8 @@ github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4=
|
|||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/machinebox/progress v0.2.0/go.mod h1:hl4FywxSjfmkmCrersGhmJH7KwuKl+Ueq9BXkOny+iE=
|
||||
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||
|
@ -408,6 +410,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXL
|
|||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/ugjka/go-tz.v2 v2.0.8 h1:EmQ1tY6aa9upe1EDqyPdyHgM9STimr2fw7+b/CuMQ94=
|
||||
gopkg.in/ugjka/go-tz.v2 v2.0.8/go.mod h1:l93M5EnjrSTTaABtGKIqNcl+z08IYbVDS7v1VA8+PgU=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
@ -42,6 +42,7 @@ type Photo struct {
|
|||
LocationChanged bool
|
||||
LocationEstimated bool
|
||||
TakenAt time.Time `gorm:"index;"`
|
||||
TakenAtLocal time.Time
|
||||
TakenAtChanged bool
|
||||
TimeZone string
|
||||
Labels []*PhotoLabel
|
||||
|
|
|
@ -9,29 +9,31 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/dsoprea/go-exif"
|
||||
"gopkg.in/ugjka/go-tz.v2/tz"
|
||||
)
|
||||
|
||||
// Exif returns information about a single image.
|
||||
type Exif struct {
|
||||
UUID string
|
||||
TakenAt time.Time
|
||||
TimeZone string
|
||||
Artist string
|
||||
CameraMake string
|
||||
CameraModel string
|
||||
LensMake string
|
||||
LensModel string
|
||||
FocalLength int
|
||||
Exposure string
|
||||
Aperture float64
|
||||
Iso int
|
||||
Lat float64
|
||||
Long float64
|
||||
Altitude int
|
||||
Width int
|
||||
Height int
|
||||
Orientation int
|
||||
All map[string]string
|
||||
UUID string
|
||||
TakenAt time.Time
|
||||
TakenAtLocal time.Time
|
||||
TimeZone string
|
||||
Artist string
|
||||
CameraMake string
|
||||
CameraModel string
|
||||
LensMake string
|
||||
LensModel string
|
||||
FocalLength int
|
||||
Exposure string
|
||||
Aperture float64
|
||||
Iso int
|
||||
Lat float64
|
||||
Long float64
|
||||
Altitude int
|
||||
Width int
|
||||
Height int
|
||||
Orientation int
|
||||
All map[string]string
|
||||
}
|
||||
|
||||
var im *exif.IfdMapping
|
||||
|
@ -178,14 +180,6 @@ func (m *MediaFile) Exif() (result *Exif, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if value, ok := tags["DateTimeOriginal"]; ok {
|
||||
m.exifData.TakenAt, _ = time.Parse("2006:01:02 15:04:05", value)
|
||||
}
|
||||
|
||||
if value, ok := tags["TimeZoneOffset"]; ok {
|
||||
m.exifData.TimeZone = value
|
||||
}
|
||||
|
||||
if value, ok := tags["ImageUniqueID"]; ok {
|
||||
m.exifData.UUID = value
|
||||
}
|
||||
|
@ -224,6 +218,34 @@ func (m *MediaFile) Exif() (result *Exif, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if m.exifData.Lat != 0 && m.exifData.Long != 0 {
|
||||
zones, err := tz.GetZone(tz.Point{
|
||||
Lat: m.exifData.Lat,
|
||||
Lon: m.exifData.Long,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
m.exifData.TimeZone = "UTC"
|
||||
}
|
||||
|
||||
m.exifData.TimeZone = zones[0]
|
||||
}
|
||||
|
||||
if value, ok := tags["DateTimeOriginal"]; ok {
|
||||
m.exifData.TakenAtLocal, _ = time.Parse("2006:01:02 15:04:05", value)
|
||||
|
||||
loc, err := time.LoadLocation(m.exifData.TimeZone)
|
||||
|
||||
if err != nil {
|
||||
m.exifData.TakenAt = m.exifData.TakenAtLocal
|
||||
log.Warnf("no location for timezone: %s", err.Error())
|
||||
} else if tl, err := time.ParseInLocation("2006:01:02 15:04:05", value, loc); err == nil {
|
||||
m.exifData.TakenAt = tl.UTC()
|
||||
} else {
|
||||
log.Warnf("could parse time: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
m.exifData.All = tags
|
||||
|
||||
return m.exifData, nil
|
||||
|
|
|
@ -22,6 +22,9 @@ func TestMediaFile_Exif_JPEG(t *testing.T) {
|
|||
assert.IsType(t, &Exif{}, info)
|
||||
|
||||
assert.Equal(t, "Canon EOS 6D", info.CameraModel)
|
||||
assert.Equal(t, "Africa/Johannesburg", info.TimeZone)
|
||||
t.Logf("UTC: %s", info.TakenAt.String())
|
||||
t.Logf("Local: %s", info.TakenAtLocal.String())
|
||||
}
|
||||
|
||||
func TestMediaFile_Exif_DNG(t *testing.T) {
|
||||
|
|
|
@ -134,11 +134,6 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string {
|
|||
photo.PhotoPath = filePath
|
||||
photo.PhotoName = fileBase
|
||||
|
||||
if photo.TakenAt.IsZero() && photo.TakenAtChanged == false {
|
||||
photo.TakenAt = mediaFile.DateCreated()
|
||||
photo.TimeZone = mediaFile.TimeZone()
|
||||
}
|
||||
|
||||
if jpeg, err := mediaFile.Jpeg(); err == nil {
|
||||
// Image classification labels
|
||||
labels = i.classifyImage(jpeg)
|
||||
|
@ -147,6 +142,9 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string {
|
|||
if exifData, err := jpeg.Exif(); err == nil {
|
||||
photo.PhotoLat = exifData.Lat
|
||||
photo.PhotoLong = exifData.Long
|
||||
photo.TakenAt = exifData.TakenAt
|
||||
photo.TakenAtLocal = exifData.TakenAtLocal
|
||||
photo.TimeZone = exifData.TimeZone
|
||||
photo.PhotoAltitude = exifData.Altitude
|
||||
photo.PhotoArtist = exifData.Artist
|
||||
|
||||
|
@ -167,6 +165,10 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string {
|
|||
photo.PhotoExposure = mediaFile.Exposure()
|
||||
}
|
||||
|
||||
if photo.TakenAt.IsZero() && photo.TakenAtChanged == false {
|
||||
photo.TakenAt = mediaFile.DateCreated()
|
||||
}
|
||||
|
||||
if location, err := mediaFile.Location(); err == nil {
|
||||
i.db.FirstOrCreate(location, "id = ?", location.ID)
|
||||
photo.Location = location
|
||||
|
@ -240,9 +242,9 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string {
|
|||
if photo.PhotoTitleChanged == false && photo.PhotoTitle == "" {
|
||||
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", util.Title(labels[0].Name), mediaFile.DateCreated().Format("2006"))
|
||||
} else {
|
||||
} else if !photo.TakenAtLocal.IsZero() {
|
||||
var daytimeString string
|
||||
hour := mediaFile.DateCreated().Hour()
|
||||
hour := photo.TakenAtLocal.Hour()
|
||||
|
||||
switch {
|
||||
case hour < 17:
|
||||
|
@ -253,7 +255,7 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string {
|
|||
daytimeString = "Unknown"
|
||||
}
|
||||
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", daytimeString, mediaFile.DateCreated().Format("2006"))
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", daytimeString, photo.TakenAtLocal.Format("2006"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -91,22 +91,6 @@ func (m *MediaFile) HasTimeAndPlace() bool {
|
|||
return result
|
||||
}
|
||||
|
||||
func (m *MediaFile) TimeZone() (result string) {
|
||||
if m.timeZone != "" {
|
||||
return m.timeZone
|
||||
}
|
||||
|
||||
exif, err := m.Exif()
|
||||
|
||||
if err != nil || exif.TimeZone == "" {
|
||||
result = "UTC"
|
||||
} else {
|
||||
result = exif.TimeZone
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// CameraModel returns the camera model with which the media file was created.
|
||||
func (m *MediaFile) CameraModel() string {
|
||||
info, err := m.Exif()
|
||||
|
|
|
@ -16,7 +16,7 @@ func TestMediaFile_DateCreated(t *testing.T) {
|
|||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
|
||||
assert.Nil(t, err)
|
||||
date := mediaFile.DateCreated().UTC()
|
||||
assert.Equal(t, "2018-09-10 12:16:13 +0000 UTC", date.String())
|
||||
assert.Equal(t, "2018-09-10 03:16:13 +0000 UTC", date.String())
|
||||
assert.Empty(t, err)
|
||||
})
|
||||
t.Run("canon_eos_6d.dng", func(t *testing.T) {
|
||||
|
@ -30,28 +30,11 @@ func TestMediaFile_DateCreated(t *testing.T) {
|
|||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
||||
assert.Nil(t, err)
|
||||
date := mediaFile.DateCreated().UTC()
|
||||
assert.Equal(t, "2013-11-26 15:53:55 +0000 UTC", date.String())
|
||||
assert.Equal(t, "2013-11-26 13:53:55 +0000 UTC", date.String())
|
||||
assert.Empty(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMediaFile_TimeZone(t *testing.T) {
|
||||
t.Run("/beach_wood.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/beach_wood.jpg")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "UTC", mediaFile.TimeZone())
|
||||
})
|
||||
t.Run("/iphone_7.heic", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "UTC", mediaFile.TimeZone())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMediaFile_HasTimeAndPlace(t *testing.T) {
|
||||
t.Run("/beach_wood.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
@ -209,7 +192,7 @@ func TestMediaFileCanonicalName(t *testing.T) {
|
|||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/beach_wood.jpg")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "20180111_130938_EB4B2A989C20", mediaFile.CanonicalName())
|
||||
assert.Equal(t, "20180111_110938_EB4B2A989C20", mediaFile.CanonicalName())
|
||||
}
|
||||
|
||||
func TestMediaFileCanonicalNameFromFile(t *testing.T) {
|
||||
|
|
|
@ -10,6 +10,7 @@ type PhotoSearchResult struct {
|
|||
UpdatedAt time.Time
|
||||
DeletedAt time.Time
|
||||
TakenAt time.Time
|
||||
TakenAtLocal time.Time
|
||||
TimeZone string
|
||||
PhotoPath string
|
||||
PhotoName string
|
||||
|
|
30
internal/photoprism/timezone.go
Normal file
30
internal/photoprism/timezone.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gopkg.in/ugjka/go-tz.v2/tz"
|
||||
)
|
||||
|
||||
// TimeZone returns the time zone where the photo was taken.
|
||||
func (m *MediaFile) TimeZone() (string, error) {
|
||||
meta, err := m.Exif()
|
||||
|
||||
if err != nil {
|
||||
return "UTC", errors.New("no image metadata")
|
||||
}
|
||||
|
||||
if meta.Lat == 0 && meta.Long == 0 {
|
||||
return "UTC", errors.New("no latitude and longitude in image metadata")
|
||||
}
|
||||
|
||||
zones, err := tz.GetZone(tz.Point{
|
||||
Lon: meta.Long, Lat: meta.Lat,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "UTC", errors.New("no matching zone found")
|
||||
}
|
||||
|
||||
return zones[0], nil
|
||||
}
|
23
internal/photoprism/timezone_test.go
Normal file
23
internal/photoprism/timezone_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMediaFile_TimeZone(t *testing.T) {
|
||||
t.Run("/elephants.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
img, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
zone, err := img.TimeZone()
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "Africa/Johannesburg", zone)
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue