From 2ed6880270ac76734ca2a820863877f8773c60ab Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Tue, 22 Dec 2020 07:47:16 +0100 Subject: [PATCH] Metadata: Add support for CreationDate in ExifTool JSON files #727 --- internal/meta/data.go | 4 +- internal/meta/json_exiftool.go | 6 ++ internal/meta/json_test.go | 53 ++++++++++- internal/meta/testdata/date-creation.mov.json | 95 +++++++++++++++++++ internal/meta/testdata/date-iphone8.mov.json | 89 +++++++++++++++++ 5 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 internal/meta/testdata/date-creation.mov.json create mode 100644 internal/meta/testdata/date-iphone8.mov.json diff --git a/internal/meta/data.go b/internal/meta/data.go index be82eb85d..48d76430e 100644 --- a/internal/meta/data.go +++ b/internal/meta/data.go @@ -13,8 +13,8 @@ import ( type Data struct { DocumentID string `meta:"ImageUniqueID,OriginalDocumentID,DocumentID"` InstanceID string `meta:"InstanceID,DocumentID"` - TakenAt time.Time `meta:"DateTimeOriginal,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeDigitized,DateTime"` - TakenAtLocal time.Time `meta:"DateTimeOriginal,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeDigitized,DateTime"` + TakenAt time.Time `meta:"DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeDigitized,DateTime"` + TakenAtLocal time.Time `meta:"DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeDigitized,DateTime"` TimeZone string `meta:"-"` Duration time.Duration `meta:"Duration,MediaDuration,TrackDuration"` Codec string `meta:"CompressorID,Compression,FileType"` diff --git a/internal/meta/json_exiftool.go b/internal/meta/json_exiftool.go index 4659f76eb..c42358e5e 100644 --- a/internal/meta/json_exiftool.go +++ b/internal/meta/json_exiftool.go @@ -131,6 +131,12 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) { log.Errorf("metadata: %s (exiftool)", err.Error()) // this should never happen } } + } else if _, offset := data.TakenAtLocal.Zone(); offset != 0 && !data.TakenAtLocal.IsZero() { + 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 = data.TakenAt.Round(time.Second).UTC() } if orientation, ok := jsonStrings["Orientation"]; ok && orientation != "" { diff --git a/internal/meta/json_test.go b/internal/meta/json_test.go index 4e6aa9c9a..20dbd0c9e 100644 --- a/internal/meta/json_test.go +++ b/internal/meta/json_test.go @@ -1,6 +1,7 @@ package meta import ( + "github.com/photoprism/photoprism/pkg/fs" "testing" "github.com/stretchr/testify/assert" @@ -18,8 +19,8 @@ func TestJSON(t *testing.T) { assert.Equal(t, CodecAvc1, data.Codec) assert.Equal(t, "3s", data.Duration.String()) - assert.Equal(t, "2018-09-08 17:20:14 +0000 UTC", data.TakenAtLocal.String()) - assert.Equal(t, "2018-09-08 15:20:14 +0000 UTC", data.TakenAt.String()) + assert.Equal(t, "2018-09-08 19:20:14 +0000 UTC", data.TakenAtLocal.String()) + assert.Equal(t, "2018-09-08 17:20:14 +0000 UTC", data.TakenAt.String()) assert.Equal(t, "Europe/Berlin", data.TimeZone) assert.Equal(t, 1920, data.Width) assert.Equal(t, 1080, data.Height) @@ -575,4 +576,52 @@ func TestJSON(t *testing.T) { assert.Equal(t, "iPhone 6 Plus", data.CameraModel) assert.Equal(t, "", data.LensModel) }) + + t.Run("date-creation.mov.json", func(t *testing.T) { + data, err := JSON("testdata/date-creation.mov.json", "") + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, string(fs.CodecAvc), data.Codec) + assert.Equal(t, "10s", data.Duration.String()) + assert.Equal(t, "2015-12-06 18:22:29 +0000 UTC", data.TakenAtLocal.String()) + assert.Equal(t, "2015-12-06 15:22:29 +0000 UTC", data.TakenAt.String()) + assert.Equal(t, "Europe/Moscow", data.TimeZone) + assert.Equal(t, 1920, data.Width) + assert.Equal(t, 1080, data.Height) + assert.Equal(t, 1920, data.ActualWidth()) + assert.Equal(t, 1080, data.ActualHeight()) + assert.Equal(t, 1, data.Orientation) + assert.Equal(t, float32(55.7579), data.Lat) + assert.Equal(t, float32(37.6197), data.Lng) + assert.Equal(t, "Apple", data.CameraMake) + assert.Equal(t, "iPhone 6 Plus", data.CameraModel) + assert.Equal(t, "", data.LensModel) + }) + + t.Run("date-iphone8.mov.json", func(t *testing.T) { + data, err := JSON("testdata/date-iphone8.mov.json", "") + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, string(fs.CodecHvc), data.Codec) + assert.Equal(t, "6s", data.Duration.String()) + assert.Equal(t, "2020-12-22 02:45:43 +0000 UTC", data.TakenAtLocal.String()) + assert.Equal(t, "2020-12-22 01:45:43 +0000 UTC", data.TakenAt.String()) + assert.Equal(t, "", data.TimeZone) + assert.Equal(t, 1920, data.Width) + assert.Equal(t, 1080, data.Height) + assert.Equal(t, 1080, data.ActualWidth()) + assert.Equal(t, 1920, data.ActualHeight()) + assert.Equal(t, 6, data.Orientation) + assert.Equal(t, float32(0), data.Lat) + assert.Equal(t, float32(0), data.Lng) + assert.Equal(t, "Apple", data.CameraMake) + assert.Equal(t, "iPhone 8", data.CameraModel) + assert.Equal(t, "", data.LensModel) + }) } diff --git a/internal/meta/testdata/date-creation.mov.json b/internal/meta/testdata/date-creation.mov.json new file mode 100644 index 000000000..1329a10df --- /dev/null +++ b/internal/meta/testdata/date-creation.mov.json @@ -0,0 +1,95 @@ +[{ + "SourceFile": "IMG_4561.MOV", + "ExifToolVersion": 12.00, + "FileName": "IMG_4561.MOV", + "Directory": ".", + "FileSize": "22 MB", + "FileModifyDate": "2020:07:16 19:06:26+02:00", + "FileAccessDate": "2020:12:22 02:37:08+01:00", + "FileInodeChangeDate": "2020:12:22 02:07:04+01:00", + "FilePermissions": "rw-------", + "FileType": "MOV", + "FileTypeExtension": "mov", + "MIMEType": "video/quicktime", + "MajorBrand": "Apple QuickTime (.MOV/QT)", + "MinorVersion": "0.0.0", + "CompatibleBrands": ["qt "], + "MediaDataSize": 23138648, + "MediaDataOffset": 36, + "MovieHeaderVersion": 0, + "CreateDate": "2015:12:06 15:22:29", + "ModifyDate": "2015:12:06 15:22:40", + "TimeScale": 600, + "Duration": "10.67 s", + "PreferredRate": 1, + "PreferredVolume": "100.00%", + "PreviewTime": "0 s", + "PreviewDuration": "0 s", + "PosterTime": "0 s", + "SelectionTime": "0 s", + "SelectionDuration": "0 s", + "CurrentTime": "0 s", + "NextTrackID": 5, + "TrackHeaderVersion": 0, + "TrackCreateDate": "2015:12:06 15:22:29", + "TrackModifyDate": "2015:12:06 15:22:40", + "TrackID": 1, + "TrackDuration": "10.67 s", + "TrackLayer": 0, + "TrackVolume": "100.00%", + "ImageWidth": 1920, + "ImageHeight": 1080, + "CleanApertureDimensions": "1920x1080", + "ProductionApertureDimensions": "1920x1080", + "EncodedPixelsDimensions": "1920x1080", + "GraphicsMode": "ditherCopy", + "OpColor": "32768 32768 32768", + "CompressorID": "avc1", + "SourceImageWidth": 1920, + "SourceImageHeight": 1080, + "XResolution": 72, + "YResolution": 72, + "CompressorName": "H.264", + "BitDepth": 24, + "VideoFrameRate": 29.981, + "Balance": 0, + "AudioFormat": "mp4a", + "AudioBitsPerSample": 16, + "AudioSampleRate": 44100, + "LayoutFlags": "Mono", + "AudioChannels": 1, + "PurchaseFileFormat": "mp4a", + "Warning": "[minor] The ExtractEmbedded option may find more tags in the media data", + "MatrixStructure": "1 0 0 0 1 0 0 0 1", + "ContentDescribes": "Track 1", + "MediaHeaderVersion": 0, + "MediaCreateDate": "2015:12:06 15:22:29", + "MediaModifyDate": "2015:12:06 15:22:40", + "MediaTimeScale": 600, + "MediaDuration": "10.67 s", + "MediaLanguageCode": "und", + "GenMediaVersion": 0, + "GenFlags": "0 0 0", + "GenGraphicsMode": "ditherCopy", + "GenOpColor": "32768 32768 32768", + "GenBalance": 0, + "HandlerClass": "Data Handler", + "HandlerVendorID": "Apple", + "HandlerDescription": "Core Media Data Handler", + "MetaFormat": "mebx", + "HandlerType": "Metadata Tags", + "GPSCoordinates": "55 deg 45' 28.44\" N, 37 deg 37' 10.92\" E, 135.89 m Above Sea Level", + "Make": "Apple", + "Model": "iPhone 6 Plus", + "Software": 9.1, + "CreationDate": "2015:12:06 18:22:29+03:00", + "ImageSize": "1920x1080", + "Megapixels": 2.1, + "AvgBitrate": "17.3 Mbps", + "GPSAltitude": "135.89 m", + "GPSAltitudeRef": "Above Sea Level", + "GPSLatitude": "55 deg 45' 28.44\" N", + "GPSLongitude": "37 deg 37' 10.92\" E", + "Rotation": 0, + "GPSPosition": "55 deg 45' 28.44\" N, 37 deg 37' 10.92\" E" +}] \ No newline at end of file diff --git a/internal/meta/testdata/date-iphone8.mov.json b/internal/meta/testdata/date-iphone8.mov.json new file mode 100644 index 000000000..0dc157ac5 --- /dev/null +++ b/internal/meta/testdata/date-iphone8.mov.json @@ -0,0 +1,89 @@ +[{ + "SourceFile": "IMG_6884.MOV", + "ExifToolVersion": 12.00, + "FileName": "IMG_6884.MOV", + "Directory": ".", + "FileSize": "9.6 MB", + "FileModifyDate": "2020:12:22 02:46:08+01:00", + "FileAccessDate": "2020:12:22 02:48:31+01:00", + "FileInodeChangeDate": "2020:12:22 02:46:24+01:00", + "FilePermissions": "rw-r--r--", + "FileType": "MOV", + "FileTypeExtension": "mov", + "MIMEType": "video/quicktime", + "MajorBrand": "Apple QuickTime (.MOV/QT)", + "MinorVersion": "0.0.0", + "CompatibleBrands": ["qt "], + "MediaDataSize": 10091927, + "MediaDataOffset": 36, + "MovieHeaderVersion": 0, + "CreateDate": "2020:12:22 01:46:08", + "ModifyDate": "2020:12:22 01:46:08", + "TimeScale": 600, + "Duration": "6.83 s", + "PreferredRate": 1, + "PreferredVolume": "100.00%", + "PreviewTime": "0 s", + "PreviewDuration": "0 s", + "PosterTime": "0 s", + "SelectionTime": "0 s", + "SelectionDuration": "0 s", + "CurrentTime": "0 s", + "NextTrackID": 5, + "TrackHeaderVersion": 0, + "TrackCreateDate": "2020:12:22 01:46:08", + "TrackModifyDate": "2020:12:22 01:46:08", + "TrackID": 1, + "TrackDuration": "6.83 s", + "TrackLayer": 0, + "TrackVolume": "0.00%", + "ImageWidth": 1920, + "ImageHeight": 1080, + "CleanApertureDimensions": "1920x1080", + "ProductionApertureDimensions": "1920x1080", + "EncodedPixelsDimensions": "1920x1080", + "GraphicsMode": "ditherCopy", + "OpColor": "32768 32768 32768", + "CompressorID": "hvc1", + "SourceImageWidth": 1920, + "SourceImageHeight": 1080, + "XResolution": 72, + "YResolution": 72, + "CompressorName": "HEVC", + "BitDepth": 24, + "VideoFrameRate": 60, + "Balance": 0, + "AudioFormat": "mp4a", + "AudioChannels": 1, + "AudioBitsPerSample": 16, + "AudioSampleRate": 44100, + "PurchaseFileFormat": "mp4a", + "Warning": "[minor] The ExtractEmbedded option may find more tags in the media data", + "MatrixStructure": "1 0 0 0 1 0 0 0 1", + "ContentDescribes": "Track 1", + "MediaHeaderVersion": 0, + "MediaCreateDate": "2020:12:22 01:46:08", + "MediaModifyDate": "2020:12:22 01:46:08", + "MediaTimeScale": 600, + "MediaDuration": "6.83 s", + "MediaLanguageCode": "und", + "GenMediaVersion": 0, + "GenFlags": "0 0 0", + "GenGraphicsMode": "ditherCopy", + "GenOpColor": "32768 32768 32768", + "GenBalance": 0, + "HandlerClass": "Data Handler", + "HandlerVendorID": "Apple", + "HandlerDescription": "Core Media Data Handler", + "MetaFormat": "mebx", + "HandlerType": "Metadata Tags", + "LocationAccuracyHorizontal": 65.000000, + "Make": "Apple", + "Model": "iPhone 8", + "Software": 14.3, + "CreationDate": "2020:12:22 02:45:43+01:00", + "ImageSize": "1920x1080", + "Megapixels": 2.1, + "AvgBitrate": "11.8 Mbps", + "Rotation": 90 +}] \ No newline at end of file