diff --git a/go.mod b/go.mod index 0c6f5412fe59a40426165441053f7b5cfa02a4b8..8eff8a6eca97284ed5043c87877bf3810f296ad1 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 require ( github.com/Masterminds/semver v1.5.0 github.com/appleboy/go-fcm v0.1.4 + github.com/bradfitz/latlong v0.0.0-20170410180902-f3db6d0dff40 github.com/cozy/goexif2 v0.0.0-20180125141006-830968571cff github.com/cozy/gomail v0.0.0-20170313100128-1395d9a6a6c0 github.com/cozy/httpcache v0.0.0-20180914105234-d3dc4988de66 diff --git a/go.sum b/go.sum index 954dad5c694460c66c0d27d7740432cefca64419..9b9bfcb40339e95187f3e3a42384597ec218be18 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bradfitz/latlong v0.0.0-20170410180902-f3db6d0dff40 h1:wsnz4B2CSHJ09pwtMReU/GRqWDsI7XSasq7Nphem3Xk= +github.com/bradfitz/latlong v0.0.0-20170410180902-f3db6d0dff40/go.mod h1:ZcXX9BndVQx6Q/JM6B8x7dLE9sl20S+TQsv4KO7tEQk= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/model/vfs/metadata.go b/model/vfs/metadata.go index a27fb1a0cfa44bb7351c84b290f41acb01320b2f..4fd893ae0bd4bd3031923cf38f8339c11bcb867d 100644 --- a/model/vfs/metadata.go +++ b/model/vfs/metadata.go @@ -2,11 +2,14 @@ package vfs import ( "bytes" + "errors" "fmt" "image" "io" "io/ioutil" "math" + "strings" + "sync" "time" // Packages image/... are not used explicitly in the code below, @@ -18,7 +21,9 @@ import ( // Same for image/webp _ "golang.org/x/image/webp" + "github.com/bradfitz/latlong" "github.com/cozy/goexif2/exif" + "github.com/cozy/goexif2/tiff" "github.com/dhowden/tag" ) @@ -222,8 +227,10 @@ func (e *ExifExtractor) Result() Metadata { x := <-e.ch switch x := x.(type) { case *exif.Exif: + localTZ := false if dt, err := x.DateTime(); err == nil { m["datetime"] = dt + localTZ = dt.Location() == time.Local } if flash, err := x.Flash(); err == nil { m["flash"] = flash @@ -234,6 +241,13 @@ func (e *ExifExtractor) Result() Metadata { "lat": lat, "long": long, } + if localTZ { + if loc := lookupLocation(latlong.LookupZoneName(lat, long)); loc != nil { + if t, err := exifDateTimeInLocation(x, loc); err == nil { + m["datetime"] = t + } + } + } } } if _, ok := m["width"]; !ok { @@ -259,6 +273,57 @@ func (e *ExifExtractor) Result() Metadata { return m } +// Code taken from perkeep +// https://github.com/perkeep/perkeep/blob/7f17c0483f2e86575ed87aac35fb75154b16b7f4/pkg/schema/schema.go#L1043-L1094 + +// This is basically a copy of the exif.Exif.DateTime() method, except: +// * it takes a *time.Location to assume +// * the caller already assumes there's no timezone offset or GPS time +// in the EXIF, so any of that code can be ignored. +func exifDateTimeInLocation(x *exif.Exif, loc *time.Location) (time.Time, error) { + tag, err := x.Get(exif.DateTimeOriginal) + if err != nil { + tag, err = x.Get(exif.DateTime) + if err != nil { + return time.Time{}, err + } + } + if tag.Format() != tiff.StringVal { + return time.Time{}, errors.New("DateTime[Original] not in string format") + } + const exifTimeLayout = "2006:01:02 15:04:05" + dateStr := strings.TrimRight(string(tag.Val), "\x00") + return time.ParseInLocation(exifTimeLayout, dateStr, loc) +} + +var zoneCache struct { + sync.RWMutex + m map[string]*time.Location +} + +func lookupLocation(zone string) *time.Location { + if zone == "" { + return nil + } + zoneCache.RLock() + l, ok := zoneCache.m[zone] + zoneCache.RUnlock() + if ok { + return l + } + loc, err := time.LoadLocation(zone) + zoneCache.Lock() + if zoneCache.m == nil { + zoneCache.m = make(map[string]*time.Location) + } + zoneCache.m[zone] = loc // even if nil + zoneCache.Unlock() + if err != nil { + return nil + } + return loc +} + // AudioExtractor is used to extract album/artist/etc. from audio type AudioExtractor struct { w *io.PipeWriter