Videos: Extract local time from DateTimeOriginal if possible #2640
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
e65c260656
commit
3403c50c48
|
@ -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:"-"`
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
|
@ -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
215
internal/meta/testdata/MVI_1724.MOV.json
vendored
Normal 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
|
||||||
|
}]
|
Loading…
Reference in a new issue