2020-05-13 20:53:15 +02:00
|
|
|
package meta
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"path/filepath"
|
|
|
|
"reflect"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
|
|
|
"github.com/tidwall/gjson"
|
2020-05-14 11:57:26 +02:00
|
|
|
"gopkg.in/ugjka/go-tz.v2/tz"
|
2020-05-13 20:53:15 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// JSON parses a json sidecar file (as used by Exiftool) and returns a Data struct.
|
2020-06-04 14:56:27 +02:00
|
|
|
func JSON(jsonName, originalName string) (data Data, err error) {
|
|
|
|
err = data.JSON(jsonName, originalName)
|
2020-05-13 20:53:15 +02:00
|
|
|
|
|
|
|
return data, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// JSON parses a json sidecar file (as used by Exiftool) and returns a Data struct.
|
2020-06-04 14:56:27 +02:00
|
|
|
func (data *Data) JSON(jsonName, originalName string) (err error) {
|
2020-05-13 20:53:15 +02:00
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
2020-06-29 13:16:55 +02:00
|
|
|
err = fmt.Errorf("metadata: %s (json panic)", e)
|
2020-05-13 20:53:15 +02:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if data.All == nil {
|
|
|
|
data.All = make(map[string]string)
|
|
|
|
}
|
|
|
|
|
2020-06-04 14:56:27 +02:00
|
|
|
jsonString, err := ioutil.ReadFile(jsonName)
|
2020-05-13 20:53:15 +02:00
|
|
|
|
|
|
|
if err != nil {
|
2020-06-29 13:16:55 +02:00
|
|
|
log.Warnf("metadata: %s (json)", err.Error())
|
2020-06-04 14:56:27 +02:00
|
|
|
return fmt.Errorf("can't read %s (json)", txt.Quote(filepath.Base(jsonName)))
|
2020-05-13 20:53:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
j := gjson.GetBytes(jsonString, "@flatten|@join")
|
|
|
|
|
|
|
|
if !j.IsObject() {
|
2020-06-29 13:16:55 +02:00
|
|
|
return fmt.Errorf("metadata: data is not an object in %s (json)", txt.Quote(filepath.Base(jsonName)))
|
2020-05-13 20:53:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
jsonValues := j.Map()
|
|
|
|
|
|
|
|
for key, val := range jsonValues {
|
|
|
|
data.All[key] = val.String()
|
|
|
|
}
|
|
|
|
|
2020-06-04 14:56:27 +02:00
|
|
|
if fileName, ok := data.All["FileName"]; ok && fileName != "" && originalName != "" && fileName != originalName {
|
2020-06-29 13:16:55 +02:00
|
|
|
return fmt.Errorf("metadata: original name %s does not match %s (json)", txt.Quote(originalName), txt.Quote(fileName))
|
2020-06-04 14:56:27 +02:00
|
|
|
}
|
|
|
|
|
2020-05-13 20:53:15 +02:00
|
|
|
v := reflect.ValueOf(data).Elem()
|
|
|
|
|
|
|
|
// Iterate through all config fields
|
|
|
|
for i := 0; i < v.NumField(); i++ {
|
|
|
|
fieldValue := v.Field(i)
|
|
|
|
|
|
|
|
tagData := v.Type().Field(i).Tag.Get("meta")
|
|
|
|
|
|
|
|
// Automatically assign values to fields with "flag" tag
|
|
|
|
if tagData != "" {
|
|
|
|
tagValues := strings.Split(tagData, ",")
|
|
|
|
|
|
|
|
var jsonValue gjson.Result
|
|
|
|
var tagValue string
|
|
|
|
|
|
|
|
for _, tagValue = range tagValues {
|
|
|
|
if r, ok := jsonValues[tagValue]; !ok {
|
|
|
|
continue
|
|
|
|
} else {
|
|
|
|
jsonValue = r
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !jsonValue.Exists() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
switch t := fieldValue.Interface().(type) {
|
|
|
|
case time.Time:
|
|
|
|
if tv, err := time.Parse("2006:01:02 15:04:05", strings.TrimSpace(jsonValue.String())); err == nil {
|
2020-05-14 11:57:26 +02:00
|
|
|
fieldValue.Set(reflect.ValueOf(tv.Round(time.Second).UTC()))
|
2020-05-13 20:53:15 +02:00
|
|
|
}
|
|
|
|
case time.Duration:
|
2020-05-14 11:57:26 +02:00
|
|
|
fieldValue.Set(reflect.ValueOf(StringToDuration(jsonValue.String())))
|
2020-05-13 20:53:15 +02:00
|
|
|
case int, int64:
|
|
|
|
fieldValue.SetInt(jsonValue.Int())
|
|
|
|
case float32, float64:
|
|
|
|
fieldValue.SetFloat(jsonValue.Float())
|
|
|
|
case uint, uint64:
|
|
|
|
fieldValue.SetUint(jsonValue.Uint())
|
|
|
|
case string:
|
|
|
|
fieldValue.SetString(strings.TrimSpace(jsonValue.String()))
|
|
|
|
case bool:
|
|
|
|
fieldValue.SetBool(jsonValue.Bool())
|
|
|
|
default:
|
2020-06-29 13:16:55 +02:00
|
|
|
log.Warnf("metadata: can't assign value of type %s to %s (json)", t, tagValue)
|
2020-05-13 20:53:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-14 11:57:26 +02:00
|
|
|
// Calculate latitude and longitude if exists.
|
|
|
|
if data.GPSPosition != "" {
|
|
|
|
data.Lat, data.Lng = GpsToLatLng(data.GPSPosition)
|
|
|
|
} else if data.GPSLatitude != "" && data.GPSLongitude != "" {
|
|
|
|
data.Lat = GpsToDecimal(data.GPSLatitude)
|
|
|
|
data.Lng = GpsToDecimal(data.GPSLongitude)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set time zone and calculate UTC time.
|
|
|
|
if data.Lat != 0 && data.Lng != 0 {
|
|
|
|
zones, err := tz.GetZone(tz.Point{
|
|
|
|
Lat: float64(data.Lat),
|
|
|
|
Lon: float64(data.Lng),
|
|
|
|
})
|
|
|
|
|
|
|
|
if err == nil && len(zones) > 0 {
|
|
|
|
data.TimeZone = zones[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
if !data.TakenAtLocal.IsZero() {
|
|
|
|
if loc, err := time.LoadLocation(data.TimeZone); err != nil {
|
2020-06-29 13:16:55 +02:00
|
|
|
log.Warnf("metadata: unknown time zone %s (json)", data.TimeZone)
|
2020-05-14 11:57:26 +02:00
|
|
|
} else if tl, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), loc); err == nil {
|
|
|
|
data.TakenAt = tl.Round(time.Second).UTC()
|
|
|
|
} else {
|
2020-06-29 13:16:55 +02:00
|
|
|
log.Errorf("metadata: %s (json)", err.Error()) // this should never happen
|
2020-05-14 11:57:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-04 14:56:27 +02:00
|
|
|
if orientation, ok := data.All["Orientation"]; ok && orientation != "" {
|
|
|
|
switch orientation {
|
|
|
|
case "1", "Horizontal (normal)":
|
|
|
|
data.Orientation = 1
|
|
|
|
case "2":
|
|
|
|
data.Orientation = 2
|
|
|
|
case "3", "Rotate 180 CW":
|
|
|
|
data.Orientation = 3
|
|
|
|
case "4":
|
|
|
|
data.Orientation = 4
|
|
|
|
case "5":
|
|
|
|
data.Orientation = 5
|
|
|
|
case "6", "Rotate 90 CW":
|
|
|
|
data.Orientation = 6
|
|
|
|
case "7":
|
|
|
|
data.Orientation = 7
|
|
|
|
case "8", "Rotate 270 CW":
|
|
|
|
data.Orientation = 8
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if data.Orientation == 0 {
|
|
|
|
// Set orientation based on rotation.
|
|
|
|
switch data.Rotation {
|
|
|
|
case 0:
|
|
|
|
data.Orientation = 1
|
|
|
|
case -180, 180:
|
|
|
|
data.Orientation = 3
|
|
|
|
case 90:
|
|
|
|
data.Orientation = 6
|
|
|
|
case -90, 270:
|
|
|
|
data.Orientation = 8
|
|
|
|
}
|
2020-05-14 14:28:23 +02:00
|
|
|
}
|
|
|
|
|
2020-05-15 09:39:32 +02:00
|
|
|
// Normalize compression information.
|
|
|
|
data.Codec = strings.ToLower(data.Codec)
|
|
|
|
if strings.Contains(data.Codec, CodecJpeg) {
|
|
|
|
data.Codec = CodecJpeg
|
|
|
|
}
|
|
|
|
|
2020-05-27 13:40:21 +02:00
|
|
|
// Validate and normalize optional DocumentID.
|
|
|
|
if len(data.DocumentID) > 0 {
|
|
|
|
data.DocumentID = SanitizeUID(data.DocumentID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate and normalize optional InstanceID.
|
|
|
|
if len(data.InstanceID) > 0 {
|
|
|
|
data.InstanceID = SanitizeUID(data.InstanceID)
|
|
|
|
}
|
|
|
|
|
2020-06-27 14:15:25 +02:00
|
|
|
data.Title = SanitizeTitle(data.Title)
|
|
|
|
data.Description = SanitizeDescription(data.Description)
|
|
|
|
|
2020-05-13 20:53:15 +02:00
|
|
|
return nil
|
|
|
|
}
|