v0.8.0: enrich XMP metadata
This commit is contained in:
@@ -9,6 +9,7 @@ type Bridge interface {
|
||||
RequestAccess() error
|
||||
ListAlbums() ([]Album, error)
|
||||
ListAssets(albumID string) ([]Asset, int, error)
|
||||
ReverseGeocode(latitude, longitude float64) (Placemark, error)
|
||||
ListTree() ([]CollectionNode, error)
|
||||
ExportPreview(assetID, outputDir string, targetSize, quality, index int) (ExportResult, error)
|
||||
ExportOriginal(assetID, outputDir string, index int) (ExportResult, error)
|
||||
@@ -39,6 +40,17 @@ func ParseAssetsJSON(jsonStr string) ([]Asset, int, error) {
|
||||
return resp.Assets, resp.Total, nil
|
||||
}
|
||||
|
||||
func ParsePlacemarkJSON(jsonStr string) (Placemark, error) {
|
||||
var resp PlacemarkResponse
|
||||
if err := json.Unmarshal([]byte(jsonStr), &resp); err != nil {
|
||||
return Placemark{}, err
|
||||
}
|
||||
if resp.Error != "" {
|
||||
return Placemark{}, fmt.Errorf("%s", resp.Error)
|
||||
}
|
||||
return resp.Placemark, nil
|
||||
}
|
||||
|
||||
func ParseTreeJSON(jsonStr string) ([]CollectionNode, error) {
|
||||
var errResp ErrorResponse
|
||||
if err := json.Unmarshal([]byte(jsonStr), &errResp); err == nil && errResp.Error != "" {
|
||||
|
||||
@@ -4,7 +4,7 @@ package photos
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I${SRCDIR}/../../bridge
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../../bridge -lphotokit_bridge -framework Photos -framework Foundation -framework AppKit -framework UniformTypeIdentifiers
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../../bridge -lphotokit_bridge -framework Photos -framework Foundation -framework AppKit -framework UniformTypeIdentifiers -framework CoreLocation -framework MapKit
|
||||
#include "photokit_bridge.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
@@ -45,6 +45,15 @@ func (*CgoBridge) ListAssets(albumID string) ([]Asset, int, error) {
|
||||
return ParseAssetsJSON(C.GoString(cs))
|
||||
}
|
||||
|
||||
func (*CgoBridge) ReverseGeocode(latitude, longitude float64) (Placemark, error) {
|
||||
cs := C.photos_reverse_geocode_json(C.double(latitude), C.double(longitude))
|
||||
if cs == nil {
|
||||
return Placemark{}, errBridgeNil
|
||||
}
|
||||
defer C.photos_free_string(cs)
|
||||
return ParsePlacemarkJSON(C.GoString(cs))
|
||||
}
|
||||
|
||||
func (*CgoBridge) ListTree() ([]CollectionNode, error) {
|
||||
cs := C.photos_list_tree_json()
|
||||
if cs == nil {
|
||||
|
||||
@@ -12,6 +12,8 @@ void photos_test_set_access(int rc);
|
||||
void photos_test_set_albums(const char *json);
|
||||
void photos_test_set_assets(const char *json);
|
||||
void photos_test_set_tree(const char *json);
|
||||
void photos_test_set_geocode(const char *json);
|
||||
void photos_test_set_geocode_null(void);
|
||||
void photos_test_set_albums_null(void);
|
||||
void photos_test_set_assets_null(void);
|
||||
void photos_test_set_tree_null(void);
|
||||
@@ -35,6 +37,8 @@ func SetTestAccessRC(rc int) { C.photos_test_set_access(C.int(rc)
|
||||
func SetTestAlbumsJSON(json string) { C.photos_test_set_albums(C.CString(json)) }
|
||||
func SetTestAssetsJSON(json string) { C.photos_test_set_assets(C.CString(json)) }
|
||||
func SetTestTreeJSON(json string) { C.photos_test_set_tree(C.CString(json)) }
|
||||
func SetTestGeocodeJSON(json string) { C.photos_test_set_geocode(C.CString(json)) }
|
||||
func SetTestGeocodeNull() { C.photos_test_set_geocode_null() }
|
||||
func SetTestAlbumsNull() { C.photos_test_set_albums_null() }
|
||||
func SetTestAssetsNull() { C.photos_test_set_assets_null() }
|
||||
func SetTestTreeNull() { C.photos_test_set_tree_null() }
|
||||
@@ -73,6 +77,15 @@ func (*CgoBridge) ListAssets(albumID string) ([]Asset, int, error) {
|
||||
return ParseAssetsJSON(C.GoString(cs))
|
||||
}
|
||||
|
||||
func (*CgoBridge) ReverseGeocode(latitude, longitude float64) (Placemark, error) {
|
||||
cs := C.photos_reverse_geocode_json(C.double(latitude), C.double(longitude))
|
||||
if cs == nil {
|
||||
return Placemark{}, errBridgeNil
|
||||
}
|
||||
defer C.photos_free_string(cs)
|
||||
return ParsePlacemarkJSON(C.GoString(cs))
|
||||
}
|
||||
|
||||
func (*CgoBridge) ListTree() ([]CollectionNode, error) {
|
||||
cs := C.photos_list_tree_json()
|
||||
if cs == nil {
|
||||
|
||||
@@ -168,6 +168,63 @@ func TestParseAssetsJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAssetsJSONExtendedMetadata(t *testing.T) {
|
||||
created := "2024-01-01T00:00:00Z"
|
||||
modified := "2024-01-02T00:00:00Z"
|
||||
assets, total, err := ParseAssetsJSON(`{"assets":[{"id":"x1","filename":"IMG.HEIC","mediaType":"image","mediaSubtypes":["photoLive","photoHDR"],"sourceType":"userLibrary","playbackStyle":"livePhoto","pixelWidth":1,"pixelHeight":2,"creationDate":"2024-01-01T00:00:00Z","modificationDate":"2024-01-02T00:00:00Z","duration":3.5,"isFavorite":true,"isHidden":true,"hasAdjustments":true,"location":{"latitude":59.1,"longitude":18.2,"altitude":10,"horizontalAccuracy":5},"burstIdentifier":"burst","representsBurst":true,"burstSelectionTypes":["autoPick"],"adjustmentInfo":{"formatIdentifier":"fmt","formatVersion":"1","baseFilename":"base.heic"},"resources":[{"type":"adjustmentData","filename":"adj.plist","uti":"public.plist","local":true,"size":42}]}],"total":1}`)
|
||||
if err != nil || total != 1 || len(assets) != 1 {
|
||||
t.Fatalf("ParseAssetsJSON err=%v total=%d len=%d", err, total, len(assets))
|
||||
}
|
||||
a := assets[0]
|
||||
if a.CreationDate == nil || *a.CreationDate != created || a.ModificationDate == nil || *a.ModificationDate != modified {
|
||||
t.Fatalf("unexpected dates: %+v", a)
|
||||
}
|
||||
if a.Location == nil || a.Location.Latitude != 59.1 || a.Location.Longitude != 18.2 || !a.IsHidden || !a.HasAdjustments || !a.RepresentsBurst {
|
||||
t.Fatalf("unexpected extended metadata: %+v", a)
|
||||
}
|
||||
if len(a.MediaSubtypes) != 2 || a.SourceType != "userLibrary" || a.PlaybackStyle != "livePhoto" || len(a.BurstSelectionTypes) != 1 {
|
||||
t.Fatalf("unexpected type metadata: %+v", a)
|
||||
}
|
||||
if a.AdjustmentInfo == nil || a.AdjustmentInfo.FormatIdentifier != "fmt" || a.Resources[0].Size != 42 {
|
||||
t.Fatalf("unexpected adjustment/resource metadata: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePlacemarkJSON(t *testing.T) {
|
||||
p, err := ParsePlacemarkJSON(`{"placemark":{"country":"Sweden","countryCode":"SE","locality":"Stockholm","formattedAddress":"Stockholm, Sweden","areasOfInterest":["Gamla stan"]}}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Country != "Sweden" || p.CountryCode != "SE" || p.Locality != "Stockholm" || len(p.AreasOfInterest) != 1 {
|
||||
t.Fatalf("unexpected placemark: %+v", p)
|
||||
}
|
||||
if _, err := ParsePlacemarkJSON(`{"error":"geocode failed"}`); err == nil || err.Error() != "geocode failed" {
|
||||
t.Fatalf("expected geocode error, got %v", err)
|
||||
}
|
||||
if _, err := ParsePlacemarkJSON(`bad`); err == nil {
|
||||
t.Fatal("expected invalid JSON error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCgoBridgeReverseGeocode(t *testing.T) {
|
||||
SetTestGeocodeJSON(`{"placemark":{"country":"Sweden","locality":"Stockholm"}}`)
|
||||
p, err := (&CgoBridge{}).ReverseGeocode(59.3293, 18.0686)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Country != "Sweden" || p.Locality != "Stockholm" {
|
||||
t.Fatalf("unexpected placemark: %+v", p)
|
||||
}
|
||||
SetTestGeocodeJSON(`{"error":"no network"}`)
|
||||
if _, err := (&CgoBridge{}).ReverseGeocode(0, 0); err == nil || err.Error() != "no network" {
|
||||
t.Fatalf("expected geocode error, got %v", err)
|
||||
}
|
||||
SetTestGeocodeNull()
|
||||
if _, err := (&CgoBridge{}).ReverseGeocode(0, 0); err != errBridgeNil {
|
||||
t.Fatalf("expected nil bridge error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTreeJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
+57
-11
@@ -6,17 +6,40 @@ type Album struct {
|
||||
}
|
||||
|
||||
type Asset struct {
|
||||
ID string `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
Cloud string `json:"cloud"`
|
||||
MediaType string `json:"mediaType"`
|
||||
PixelWidth int `json:"pixelWidth"`
|
||||
PixelHeight int `json:"pixelHeight"`
|
||||
CreationDate *string `json:"creationDate,omitempty"`
|
||||
Duration float64 `json:"duration,omitempty"`
|
||||
IsFavorite bool `json:"isFavorite,omitempty"`
|
||||
HasAdjustments bool `json:"hasAdjustments,omitempty"`
|
||||
Resources []AssetResource `json:"resources,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
Cloud string `json:"cloud"`
|
||||
MediaType string `json:"mediaType"`
|
||||
MediaSubtypes []string `json:"mediaSubtypes,omitempty"`
|
||||
SourceType string `json:"sourceType,omitempty"`
|
||||
PlaybackStyle string `json:"playbackStyle,omitempty"`
|
||||
PixelWidth int `json:"pixelWidth"`
|
||||
PixelHeight int `json:"pixelHeight"`
|
||||
CreationDate *string `json:"creationDate,omitempty"`
|
||||
ModificationDate *string `json:"modificationDate,omitempty"`
|
||||
Duration float64 `json:"duration,omitempty"`
|
||||
IsFavorite bool `json:"isFavorite,omitempty"`
|
||||
IsHidden bool `json:"isHidden,omitempty"`
|
||||
HasAdjustments bool `json:"hasAdjustments,omitempty"`
|
||||
Location *AssetLocation `json:"location,omitempty"`
|
||||
BurstIdentifier string `json:"burstIdentifier,omitempty"`
|
||||
RepresentsBurst bool `json:"representsBurst,omitempty"`
|
||||
BurstSelectionTypes []string `json:"burstSelectionTypes,omitempty"`
|
||||
AdjustmentInfo *AdjustmentInfo `json:"adjustmentInfo,omitempty"`
|
||||
Resources []AssetResource `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
type AssetLocation struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
Altitude float64 `json:"altitude,omitempty"`
|
||||
HorizontalAccuracy float64 `json:"horizontalAccuracy,omitempty"`
|
||||
}
|
||||
|
||||
type AdjustmentInfo struct {
|
||||
FormatIdentifier string `json:"formatIdentifier,omitempty"`
|
||||
FormatVersion string `json:"formatVersion,omitempty"`
|
||||
BaseFilename string `json:"baseFilename,omitempty"`
|
||||
}
|
||||
|
||||
type AssetResource struct {
|
||||
@@ -24,6 +47,29 @@ type AssetResource struct {
|
||||
Filename string `json:"filename"`
|
||||
UTI string `json:"uti"`
|
||||
Local bool `json:"local"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
}
|
||||
|
||||
type Placemark struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
CountryCode string `json:"countryCode,omitempty"`
|
||||
AdministrativeArea string `json:"administrativeArea,omitempty"`
|
||||
SubAdministrativeArea string `json:"subAdministrativeArea,omitempty"`
|
||||
Locality string `json:"locality,omitempty"`
|
||||
SubLocality string `json:"subLocality,omitempty"`
|
||||
Thoroughfare string `json:"thoroughfare,omitempty"`
|
||||
SubThoroughfare string `json:"subThoroughfare,omitempty"`
|
||||
PostalCode string `json:"postalCode,omitempty"`
|
||||
FormattedAddress string `json:"formattedAddress,omitempty"`
|
||||
InlandWater string `json:"inlandWater,omitempty"`
|
||||
Ocean string `json:"ocean,omitempty"`
|
||||
AreasOfInterest []string `json:"areasOfInterest,omitempty"`
|
||||
}
|
||||
|
||||
type PlacemarkResponse struct {
|
||||
Placemark Placemark `json:"placemark"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type ExportResult struct {
|
||||
|
||||
Reference in New Issue
Block a user