Videos: Extract local time from DateTimeOriginal if possible #2640

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2022-08-24 17:50:22 +02:00
parent e65c260656
commit 3403c50c48
4 changed files with 268 additions and 7 deletions

View file

@ -17,8 +17,9 @@ type Data struct {
FileName string `meta:"FileName"` FileName string `meta:"FileName"`
DocumentID string `meta:"BurstUUID,MediaGroupUUID,ImageUniqueID,OriginalDocumentID,DocumentID,DigitalImageGUID"` DocumentID string `meta:"BurstUUID,MediaGroupUUID,ImageUniqueID,OriginalDocumentID,DocumentID,DigitalImageGUID"`
InstanceID string `meta:"InstanceID,DocumentID"` InstanceID string `meta:"InstanceID,DocumentID"`
TakenAt time.Time `meta:"SubSecDateTimeOriginal,SubSecDateTimeCreated,SubSecCreateDate,DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeCreated,DateTime,DateTimeDigitized" xmp:"DateCreated"` CreatedAt time.Time `meta:"SubSecCreateDate,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,TrackCreateDate,MetadataDate"`
TakenAtLocal time.Time `meta:"SubSecDateTimeOriginal,SubSecDateTimeCreated,SubSecCreateDate,DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeCreated,DateTime,DateTimeDigitized"` TakenAt time.Time `meta:"SubSecDateTimeOriginal,SubSecDateTimeCreated,DateTimeOriginal,CreationDate,DateTimeCreated,DateTime,DateTimeDigitized" xmp:"DateCreated"`
TakenAtLocal time.Time `meta:"SubSecDateTimeOriginal,SubSecDateTimeCreated,DateTimeOriginal,CreationDate,DateTimeCreated,DateTime,DateTimeDigitized"`
TakenGps time.Time `meta:"GPSDateTime,GPSDateStamp"` TakenGps time.Time `meta:"GPSDateTime,GPSDateStamp"`
TakenNs int `meta:"-"` TakenNs int `meta:"-"`
TimeZone string `meta:"-"` TimeZone string `meta:"-"`

View file

@ -33,8 +33,14 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
j := gjson.GetBytes(jsonData, "@flatten|@join") j := gjson.GetBytes(jsonData, "@flatten|@join")
logName := "json file"
if originalName != "" {
logName = clean.Log(filepath.Base(originalName))
}
if !j.IsObject() { if !j.IsObject() {
return fmt.Errorf("metadata: data is not an object in %s (exiftool)", clean.Log(filepath.Base(originalName))) return fmt.Errorf("metadata: data is not an object in %s (exiftool)", logName)
} }
data.json = make(map[string]string) data.json = make(map[string]string)
@ -46,6 +52,8 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
if fileName, ok := data.json["FileName"]; ok && fileName != "" && originalName != "" && fileName != originalName { if fileName, ok := data.json["FileName"]; ok && fileName != "" && originalName != "" && fileName != originalName {
return fmt.Errorf("metadata: original name %s does not match %s (exiftool)", clean.Log(originalName), clean.Log(fileName)) return fmt.Errorf("metadata: original name %s does not match %s (exiftool)", clean.Log(originalName), clean.Log(fileName))
} else if fileName != "" && originalName == "" {
logName = clean.Log(filepath.Base(fileName))
} }
v := reflect.ValueOf(data).Elem() v := reflect.ValueOf(data).Elem()
@ -185,18 +193,34 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
hasTimeOffset := false hasTimeOffset := false
// Fallback to GPS timestamp. // Has Media Create Date?
if !data.CreatedAt.IsZero() {
data.TakenAt = data.CreatedAt
}
// Fallback to GPS UTC Time?
if data.TakenAt.IsZero() && data.TakenAtLocal.IsZero() && !data.TakenGps.IsZero() { if data.TakenAt.IsZero() && data.TakenAtLocal.IsZero() && !data.TakenGps.IsZero() {
data.TimeZone = time.UTC.String() data.TimeZone = time.UTC.String()
data.TakenAt = data.TakenGps.UTC() data.TakenAt = data.TakenGps.UTC()
data.TakenAtLocal = time.Time{} data.TakenAtLocal = time.Time{}
} }
// Check plausibility of the local <> UTC time difference.
if !data.TakenAt.IsZero() && !data.TakenAtLocal.IsZero() {
if d := data.TakenAt.Sub(data.TakenAtLocal).Abs(); d > time.Hour*27 {
log.Warnf("metadata: invalid local time offset %.1fh in %s (exiftool)", d.Hours(), logName)
data.TakenAtLocal = data.TakenAt
data.TakenAt = data.TakenAt.UTC()
}
}
// Has time zone offset?
if _, offset := data.TakenAtLocal.Zone(); offset != 0 && !data.TakenAtLocal.IsZero() { if _, offset := data.TakenAtLocal.Zone(); offset != 0 && !data.TakenAtLocal.IsZero() {
hasTimeOffset = true hasTimeOffset = true
} else if mt, ok := data.json["MIMEType"]; ok && (mt == MimeVideoMP4 || mt == MimeQuicktime) { } else if mt, ok := data.json["MIMEType"]; ok && data.TakenAtLocal.IsZero() && (mt == MimeVideoMP4 || mt == MimeQuicktime) {
// Assume default time zone for MP4 & Quicktime videos is UTC. // Assume default time zone for MP4 & Quicktime videos is UTC.
// see https://exiftool.org/TagNames/QuickTime.html // see https://exiftool.org/TagNames/QuickTime.html
log.Debugf("metadata: using UTC as default time zone in %s (%s)", logName, clean.Log(mt))
data.TimeZone = time.UTC.String() data.TimeZone = time.UTC.String()
data.TakenAt = data.TakenAt.UTC() data.TakenAt = data.TakenAt.UTC()
data.TakenAtLocal = time.Time{} data.TakenAtLocal = time.Time{}

View file

@ -732,6 +732,26 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "", data.LensModel) assert.Equal(t, "", data.LensModel)
}) })
t.Run("MVI_1724.MOV.json", func(t *testing.T) {
data, err := JSON("testdata/MVI_1724.MOV.json", "")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, string(video.CodecAVC), data.Codec)
assert.Equal(t, "6s", data.Duration.String())
assert.Equal(t, "2022-06-25 06:50:58 +0000 UTC", data.TakenAtLocal.String())
assert.Equal(t, "2022-06-25 04:50:58 +0000 UTC", data.TakenAt.String())
assert.Equal(t, "", data.TimeZone) // Local Time
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, float32(0), data.Lat)
assert.Equal(t, float32(0), data.Lng)
assert.Equal(t, "Canon", data.CameraMake)
assert.Equal(t, "Canon PowerShot G15", data.CameraModel)
assert.Equal(t, "6.1", data.LensModel)
})
t.Run("snow.json", func(t *testing.T) { t.Run("snow.json", func(t *testing.T) {
data, err := JSON("testdata/snow.json", "") data, err := JSON("testdata/snow.json", "")
@ -845,8 +865,9 @@ func TestJSON(t *testing.T) {
// t.Logf("all: %+v", data.json) // t.Logf("all: %+v", data.json)
assert.Equal(t, "Jens\r\tMander", data.Artist) assert.Equal(t, "Jens\r\tMander", data.Artist)
assert.Equal(t, "2004-09-23T10:57:57Z", data.TakenAt.Format("2006-01-02T15:04:05Z")) assert.Equal(t, "", data.TimeZone)
assert.Equal(t, "2004-09-23T10:57:57Z", data.TakenAtLocal.Format("2006-01-02T15:04:05Z")) assert.Equal(t, "2004-10-07 20:49:16 +0000 UTC", data.TakenAt.String())
assert.Equal(t, "2004-10-07 22:49:16 +0000 UTC", data.TakenAtLocal.String())
assert.Equal(t, "This is the title", data.Title) assert.Equal(t, "This is the title", data.Title)
assert.Equal(t, "", data.Keywords.String()) assert.Equal(t, "", data.Keywords.String())
assert.Equal(t, "This is a\n\ndescription!", data.Description) assert.Equal(t, "This is a\n\ndescription!", data.Description)

215
internal/meta/testdata/MVI_1724.MOV.json vendored Normal file
View file

@ -0,0 +1,215 @@
[{
"SourceFile": "MVI_1724.MOV",
"ExifToolVersion": 12.16,
"FileName": "MVI_1724.MOV",
"Directory": ".",
"FileSize": 26861044,
"FileModifyDate": "2022:06:25 06:50:58+02:00",
"FileAccessDate": "2022:08:24 14:40:40+02:00",
"FileInodeChangeDate": "2022:08:24 14:39:39+02:00",
"FilePermissions": 664,
"FileType": "MOV",
"FileTypeExtension": "MOV",
"MIMEType": "video/quicktime",
"MajorBrand": "qt ",
"MinorVersion": "2007.9.0",
"CompatibleBrands": ["qt ","CAEP"],
"CompressorVersion": "CanonAVC0010/02.00.00/00.00.00",
"ExifByteOrder": "II",
"ImageDescription": " ",
"Orientation": 1,
"ResolutionUnit": 2,
"YCbCrPositioning": 2,
"ExposureTime": 0.01666666667,
"FNumber": 5.6,
"SensitivityType": 4,
"ExifVersion": "0230",
"DateTimeOriginal": "2022:06:25 06:50:58",
"ComponentsConfiguration": "1 2 3 0",
"CompressedBitsPerPixel": 3,
"ShutterSpeedValue": 0.0166740687605754,
"ApertureValue": 5.59591869015324,
"MaxApertureValue": 1.79470907500311,
"Flash": 16,
"FocalLength": 6.1,
"MacroMode": 2,
"SelfTimer": 0,
"Quality": 130,
"CanonFlashMode": 0,
"ContinuousDrive": 2,
"FocusMode": 4,
"RecordMode": 9,
"CanonImageSize": 142,
"EasyMode": 1,
"DigitalZoom": 0,
"Contrast": 0,
"Saturation": 0,
"Sharpness": 0,
"CameraISO": "Auto",
"MeteringMode": 3,
"FocusRange": 1,
"AFPoint": 16390,
"CanonExposureMode": 1,
"LensType": 65535,
"MaxFocalLength": 30.5,
"MinFocalLength": 6.1,
"FocalUnits": 1000,
"MaxAperture": 1.79470907500311,
"MinAperture": 8,
"FlashActivity": 0,
"FlashBits": 0,
"FocusContinuous": 1,
"AESetting": 0,
"ImageStabilization": 260,
"ZoomSourceWidth": 4000,
"ZoomTargetWidth": 4000,
"SpotMeteringMode": 0,
"ManualFlashOutput": 0,
"AutoISO": 37.7291106898356,
"BaseISO": 200,
"MeasuredEV": 11.5,
"TargetAperture": 5.59591869015324,
"TargetExposureTime": 0.0166740687605754,
"ExposureCompensation": 0,
"WhiteBalance": 0,
"SlowShutter": 0,
"SequenceNumber": 0,
"OpticalZoomCode": 0,
"FlashGuideNumber": 0,
"FlashExposureComp": 0,
"AutoExposureBracketing": 0,
"AEBBracketValue": 0,
"ControlMode": 1,
"FocusDistanceUpper": 0.65,
"FocusDistanceLower": 0,
"BulbDuration": 0,
"CameraType": 250,
"AutoRotate": 0,
"NDFilter": 0,
"SelfTimer2": 0,
"FlashOutput": 0,
"CanonImageType": "MVI:PowerShot G15 Movie",
"CanonFirmwareVersion": "Firmware Version 1.00",
"FileNumber": 1551724,
"CameraTemperature": 21,
"CanonModelID": 53673984,
"ThumbnailImageValidArea": "0 159 15 104",
"DateStampMode": 0,
"MyColorMode": 0,
"FirmwareRevision": 16778496,
"Categories": 0,
"AFAreaMode": 2,
"NumAFPoints": 9,
"ValidAFPoints": 1,
"CanonImageWidth": 1920,
"CanonImageHeight": 1080,
"AFImageWidth": 4000,
"AFImageHeight": 3000,
"AFAreaWidths": "720 18 18 18 18 2304 0 -14992 24172",
"AFAreaHeights": "540 240 240 240 240 0 19 3739 542",
"AFAreaXPositions": "0 18 -18 0 18 18 18 18 18",
"AFAreaYPositions": "0 0 240 240 240 240 240 240 240",
"AFPointsInFocus": 16,
"PrimaryAFPoint": 4,
"IntelligentContrast": 0,
"ImageUniqueID": "a5a750522f844e7b06168799cbbb9dd5",
"FacesDetected": 65535,
"TimeZone": 120,
"TimeZoneCity": 32766,
"DaylightSavings": 60,
"AspectRatio": 7,
"CroppedImageWidth": 1920,
"CroppedImageHeight": 1080,
"CroppedImageLeft": 0,
"CroppedImageTop": 0,
"VRDOffset": 0,
"UserComment": "",
"FlashpixVersion": "0100",
"ColorSpace": 1,
"ExifImageWidth": 160,
"ExifImageHeight": 120,
"InteropIndex": "THM",
"InteropVersion": "0100",
"RelatedImageWidth": 1920,
"RelatedImageHeight": 1080,
"SensingMethod": 2,
"FileSource": 3,
"CustomRendered": 0,
"ExposureMode": 0,
"DigitalZoomRatio": 1,
"SceneCaptureType": 0,
"OwnerName": "",
"GPSVersionID": "2 3 0 0",
"EncodingProcess": 0,
"BitsPerSample": 8,
"ColorComponents": 3,
"YCbCrSubSampling": "2 1",
"ThumbnailImage": "(Binary data 9850 bytes, use -b option to extract)",
"Make": "Canon",
"Model": "Canon PowerShot G15",
"UserRating": 0,
"Copyright": " ",
"Author": " ",
"MovieHeaderVersion": 0,
"CreateDate": "2022:06:25 04:50:58",
"ModifyDate": "2022:06:25 04:50:58",
"TimeScale": 24000,
"Duration": 6.08941666666667,
"PreferredRate": 1,
"PreferredVolume": 1,
"PreviewTime": 0,
"PreviewDuration": 0,
"PosterTime": 0,
"SelectionTime": 0,
"SelectionDuration": 0,
"CurrentTime": 0,
"NextTrackID": 3,
"TrackHeaderVersion": 0,
"TrackCreateDate": "2022:06:25 04:50:58",
"TrackModifyDate": "2022:06:25 04:50:58",
"TrackID": 1,
"TrackDuration": 6.08941666666667,
"TrackLayer": 0,
"TrackVolume": 0,
"ImageWidth": 1920,
"ImageHeight": 1080,
"GraphicsMode": 0,
"OpColor": "0 0 0",
"CompressorID": "avc1",
"SourceImageWidth": 1920,
"SourceImageHeight": 1080,
"XResolution": 72,
"YResolution": 72,
"BitDepth": 24,
"VideoFrameRate": 23.976023976024,
"MatrixStructure": "1 0 0 0 1 0 0 0 1",
"MediaHeaderVersion": 0,
"MediaCreateDate": "2022:06:25 04:50:58",
"MediaModifyDate": "2022:06:25 04:50:58",
"MediaTimeScale": 48000,
"MediaDuration": 6.08941666666667,
"Balance": 0,
"HandlerClass": "dhlr",
"HandlerType": "alis",
"AudioFormat": "sowt",
"AudioBitsPerSample": 16,
"AudioSampleRate": 48000,
"LayoutFlags": 101,
"AudioChannels": 2,
"MediaDataSize": 26762732,
"MediaDataOffset": 98312,
"DriveMode": 0,
"ISO": 75.4582213796711,
"Lens": 6.1,
"ShootingMode": 1,
"Aperture": 5.6,
"ImageSize": "1920 1080",
"LensID": 65535,
"Megapixels": 2.0736,
"ShutterSpeed": 0.01666666667,
"AvgBitrate": 35159666,
"Rotation": 0,
"Lens35efl": 6.1,
"FocalLength35efl": 6.1,
"LightValue": 11.2927817489393
}]