photoprism/internal/meta/json_gphotos.go
Michael Mayer 82d61d1f93 File Types: Add experimental support for animated GIFs #590 #2207
Animated GIFs are transcoded to AVC because it is much smaller and
thus also suitable for long/large animations. In addition, this commit
adds support for more metadata fields such as frame rate, number of
frames, file capture timestamp (unix milliseconds), media type,
and software version. Support for SVG files can later be implemented in
a similar way.
2022-04-13 22:17:59 +02:00

162 lines
3.4 KiB
Go

package meta
import (
"encoding/json"
"fmt"
"runtime/debug"
"time"
"gopkg.in/photoprism/go-tz.v2/tz"
)
type GPhoto struct {
Title string `json:"title"`
Description string `json:"description"`
Views int `json:"imageViews,string"`
Geo GGeo `json:"geoData"`
TakenAt GTime `json:"photoTakenTime"`
CreatedAt GTime `json:"creationTime"`
UpdatedAt GTime `json:"modificationTime"`
}
func (m GPhoto) SanitizedTitle() string {
return SanitizeTitle(m.Title)
}
func (m GPhoto) SanitizedDescription() string {
return SanitizeDescription(m.Description)
}
type GMeta struct {
Album GAlbum `json:"albumData"`
}
type GAlbum struct {
Title string `json:"title"`
Description string `json:"description"`
Access string `json:"access"`
Location string `json:"location"`
Date GTime `json:"date"`
Geo GGeo `json:"geoData"`
}
func (m GAlbum) Exists() bool {
return m.Title != ""
}
type GGeo struct {
Lat float64 `json:"latitude"`
Lng float64 `json:"longitude"`
Altitude float64 `json:"altitude"`
}
func (m GGeo) Exists() bool {
return m.Lat != 0.0 && m.Lng != 0.0
}
type GTime struct {
Unix int64 `json:"timestamp,string"`
Formatted string `json:"formatted"`
}
func (m GTime) Exists() bool {
return m.Unix > 0
}
func (m GTime) Time() time.Time {
return time.Unix(m.Unix, 0).UTC()
}
// GMeta parses JSON sidecar data as created by Google Photos.
func (data *Data) GMeta(jsonData []byte) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("metadata: %s (gmeta panic)\nstack: %s", e, debug.Stack())
}
}()
p := GMeta{}
if err := json.Unmarshal(jsonData, &p); err != nil {
return err
}
if p.Album.Exists() {
data.Albums = append(data.Albums, p.Album.Title)
}
return nil
}
// GPhoto parses JSON photo sidecar data as created by Google Photos.
func (data *Data) GPhoto(jsonData []byte) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("metadata: %s (gphoto panic)\nstack: %s", e, debug.Stack())
}
}()
p := GPhoto{}
if err := json.Unmarshal(jsonData, &p); err != nil {
return err
}
if s := p.SanitizedTitle(); s != "" && data.Title == "" {
data.Title = s
}
if s := p.SanitizedDescription(); s != "" && data.Description == "" {
data.Description = s
}
if p.Views > 0 && data.Views == 0 {
data.Views = p.Views
}
if p.TakenAt.Exists() {
if data.TakenAt.IsZero() {
data.TakenAt = p.TakenAt.Time()
}
if data.TakenAtLocal.IsZero() {
data.TakenAtLocal = p.TakenAt.Time()
}
}
if p.Geo.Exists() {
if data.Lat == 0 && data.Lng == 0 {
data.Lat = float32(p.Geo.Lat)
data.Lng = float32(p.Geo.Lng)
}
if data.Altitude == 0 {
data.Altitude = int(p.Geo.Altitude)
}
}
// 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 {
log.Warnf("metadata: unknown time zone %s (gphotos)", data.TimeZone)
} 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.Truncate(time.Second).UTC()
} else {
log.Errorf("metadata: %s (gphotos)", err.Error()) // this should never happen
}
}
}
return nil
}