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"`
DocumentID string `meta:"BurstUUID,MediaGroupUUID,ImageUniqueID,OriginalDocumentID,DocumentID,DigitalImageGUID"`
InstanceID string `meta:"InstanceID,DocumentID"`
TakenAt time.Time `meta:"SubSecDateTimeOriginal,SubSecDateTimeCreated,SubSecCreateDate,DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeCreated,DateTime,DateTimeDigitized" xmp:"DateCreated"`
TakenAtLocal time.Time `meta:"SubSecDateTimeOriginal,SubSecDateTimeCreated,SubSecCreateDate,DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeCreated,DateTime,DateTimeDigitized"`
CreatedAt time.Time `meta:"SubSecCreateDate,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,TrackCreateDate,MetadataDate"`
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"`
TakenNs int `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")
logName := "json file"
if originalName != "" {
logName = clean.Log(filepath.Base(originalName))
}
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)
@ -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 {
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()
@ -185,18 +193,34 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
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() {
data.TimeZone = time.UTC.String()
data.TakenAt = data.TakenGps.UTC()
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() {
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.
// 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.TakenAt = data.TakenAt.UTC()
data.TakenAtLocal = time.Time{}

View File

@ -732,6 +732,26 @@ func TestJSON(t *testing.T) {
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) {
data, err := JSON("testdata/snow.json", "")
@ -845,8 +865,9 @@ func TestJSON(t *testing.T) {
// t.Logf("all: %+v", data.json)
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, "2004-09-23T10:57:57Z", data.TakenAtLocal.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "", data.TimeZone)
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, "", data.Keywords.String())
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
}]