diff --git a/examples/meta/main.go b/examples/meta/main.go index 4781da4..0512742 100644 --- a/examples/meta/main.go +++ b/examples/meta/main.go @@ -7,6 +7,7 @@ import ( "time" "github.com/recentralized/structure/data" + "github.com/recentralized/structure/index" "github.com/recentralized/structure/meta" ) @@ -28,6 +29,8 @@ func main() { func buildMeta() (*meta.Meta, error) { + srcID := index.SrcID("e8400c72-f7d0-53f9-98ca-ee23238231fe") + doc := meta.New() doc.Type = data.JPG doc.Size = 1024 @@ -42,9 +45,13 @@ func buildMeta() (*meta.Meta, error) { "ExposureTime": meta.ExifValue{ID: "ShutterSpeed", Val: "1/60"}, }, } - doc.Sidecar = meta.Content{ - Exif: meta.Exif{ - "FNumber": meta.ExifValue{ID: "0x829d", Val: 1.8}, + doc.Src = map[index.SrcID]meta.SrcSpecific{ + srcID: { + Sidecar: &meta.Content{ + Exif: meta.Exif{ + "FNumber": meta.ExifValue{ID: "0x829d", Val: 1.8}, + }, + }, }, } diff --git a/examples/meta/output.json b/examples/meta/output.json index 04ff190..425ff87 100644 --- a/examples/meta/output.json +++ b/examples/meta/output.json @@ -15,11 +15,15 @@ } } }, - "sidecar": { - "exif": { - "FNumber": { - "id": "0x829d", - "val": 1.8 + "src": { + "e8400c72-f7d0-53f9-98ca-ee23238231fe": { + "sidecar": { + "exif": { + "FNumber": { + "id": "0x829d", + "val": 1.8 + } + } } } } diff --git a/meta/encoding.go b/meta/encoding.go index 86bb4cb..98c8b7b 100644 --- a/meta/encoding.go +++ b/meta/encoding.go @@ -5,6 +5,7 @@ import ( "time" "github.com/recentralized/structure/data" + "github.com/recentralized/structure/index" ) type metaJSON struct { @@ -12,24 +13,34 @@ type metaJSON struct { Type data.Type `json:"type"` ContentType data.Type `json:"content_type,omitempty"` Size int64 `json:"size"` - Inherent *Content `json:"inherent,omitempty"` - Sidecar *Content `json:"sidecar,omitempty"` - SrcSpecific // Embedded fields. + + Inherent *Content `json:"inherent,omitempty"` + Src map[index.SrcID]SrcSpecific `json:"src,omitempty"` + + // V0 Fields + Sidecar *Content `json:"sidecar,omitempty"` + V0SrcSpecific // Embedded fields. } // MarshalJSON converts Meta to JSON. func (m Meta) MarshalJSON() ([]byte, error) { j := metaJSON{ - Version: m.Version, - Type: m.Type, - Size: m.Size, - SrcSpecific: m.Srcs, + Version: m.Version, + Type: m.Type, + Size: m.Size, + V0SrcSpecific: m.V0Srcs, } if !m.Inherent.isZero() { j.Inherent = &m.Inherent } - if !m.Sidecar.isZero() { - j.Sidecar = &m.Sidecar + if !m.V0Sidecar.isZero() { + j.Sidecar = &m.V0Sidecar + } + if m.Src != nil { + j.Src = make(map[index.SrcID]SrcSpecific) + for k, v := range m.Src { + j.Src[k] = v + } } return json.Marshal(j) } @@ -46,13 +57,19 @@ func (m *Meta) UnmarshalJSON(data []byte) error { m.Type = j.ContentType } m.Size = j.Size + if j.Src != nil { + m.Src = make(map[index.SrcID]SrcSpecific) + for k, v := range j.Src { + m.Src[k] = v + } + } if j.Inherent != nil { m.Inherent = *j.Inherent } if j.Sidecar != nil { - m.Sidecar = *j.Sidecar + m.V0Sidecar = *j.Sidecar } - m.Srcs = j.SrcSpecific + m.V0Srcs = j.V0SrcSpecific return nil } diff --git a/meta/encoding_test.go b/meta/encoding_test.go index f53e663..2eaaf76 100644 --- a/meta/encoding_test.go +++ b/meta/encoding_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/recentralized/structure/data" + "github.com/recentralized/structure/index" ) func TestMetaJSON(t *testing.T) { @@ -28,6 +29,13 @@ func TestMetaJSON(t *testing.T) { Version: "v1", Type: data.JPG, Size: 100, + }, + json: `{"version":"v1","type":"jpg","size":100}`, + }, + { + desc: "inherent", + meta: Meta{ + Version: "v1", Inherent: Content{ Created: time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC), Image: Image{ @@ -41,23 +49,66 @@ func TestMetaJSON(t *testing.T) { }, }, }, - Sidecar: Content{ - Created: time.Date(2, 2, 3, 4, 5, 6, 7, time.UTC), + }, + json: `{"version":"v1","type":"","size":0,"inherent":{"created":"0001-02-03T04:05:06.000000007Z","image":{"width":100,"height":60},"exif":{"CreateData":{"id":"0x9004","val":"2013:07:17 19:59:58"}}}}`, + }, + { + desc: "src-specific: sidecar", + meta: Meta{ + Version: "v1", + Src: map[index.SrcID]SrcSpecific{ + index.SrcID("s1"): { + Sidecar: &Content{ + Created: time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC), + Image: Image{ + Width: 100, + Height: 60, + }, + Exif: Exif{ + "CreateData": ExifValue{ + ID: "0x9004", + Val: "2013:07:17 19:59:58", + }, + }, + }, + }, }, }, - json: `{"version":"v1","type":"jpg","size":100,"inherent":{"created":"0001-02-03T04:05:06.000000007Z","image":{"width":100,"height":60},"exif":{"CreateData":{"id":"0x9004","val":"2013:07:17 19:59:58"}}},"sidecar":{"created":"0002-02-03T04:05:06.000000007Z"}}`, + json: `{"version":"v1","type":"","size":0,"src":{"s1":{"sidecar":{"created":"0001-02-03T04:05:06.000000007Z","image":{"width":100,"height":60},"exif":{"CreateData":{"id":"0x9004","val":"2013:07:17 19:59:58"}}}}}}`, }, { - desc: "src-specific fields: flickr", + desc: "src-specific: flickr", meta: Meta{ Version: "v1", - Srcs: SrcSpecific{ - Flickr: &FlickrMedia{ - ID: "123", + Src: map[index.SrcID]SrcSpecific{ + index.SrcID("s1"): { + Flickr: &FlickrMedia{ + ID: "123", + }, }, }, }, - json: `{"version":"v1","type":"","size":0,"flickr":{"id":"123"}}`, + json: `{"version":"v1","type":"","size":0,"src":{"s1":{"flickr":{"id":"123"}}}}`, + }, + { + desc: "v0 sidecar", + meta: Meta{ + Version: "v1", + V0Sidecar: Content{ + Created: time.Date(2, 2, 3, 4, 5, 6, 7, time.UTC), + }, + }, + json: `{"version":"v1","type":"","size":0,"sidecar":{"created":"0002-02-03T04:05:06.000000007Z"}}`, + }, + { + desc: "v0 src-specific", + meta: Meta{ + Version: "v1", + V0Srcs: V0SrcSpecific{ + //Flickr: + }, + }, + json: `{"version":"v1","type":"","size":0}`, }, } for _, tt := range tests { diff --git a/meta/meta.go b/meta/meta.go index 0ee976d..b5f74b9 100644 --- a/meta/meta.go +++ b/meta/meta.go @@ -4,9 +4,11 @@ import ( "encoding/json" "errors" "io" + "sort" "time" "github.com/recentralized/structure/data" + "github.com/recentralized/structure/index" ) const ( @@ -34,12 +36,17 @@ type Meta struct { // Metadata that came from the content itself. Inherent Content + // Metadata that came from elsewhere on the source. + Src map[index.SrcID]SrcSpecific + // Metadata that came from nearby, such as an XMP sidecar file or other // source of metadata. - Sidecar Content + // Deprecated: Read from V0 only. Use Src[srcID] instead. + V0Sidecar Content // Metadata that came from the source of the data. - Srcs SrcSpecific + // Deprecated: Read from V0 only. Use Src[srcID] instead. + V0Srcs V0SrcSpecific } // New initializes a new Meta at the current version. @@ -70,9 +77,17 @@ func ParseJSON(r io.Reader) (*Meta, error) { // time available it returns time.Time's zero value. func (m *Meta) DateCreated() time.Time { times := []time.Time{ - m.Sidecar.Created, + m.V0Sidecar.Created, m.Inherent.Created, } + if len(m.Src) > 0 { + for _, v := range m.Src { + if v.Sidecar != nil { + times = append(times, v.Sidecar.Created) + } + } + sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) }) + } for _, t := range times { if !t.IsZero() { return t @@ -102,7 +117,13 @@ type Image struct { // SrcSpecific contains source-specific metadata. type SrcSpecific struct { - Flickr *FlickrMedia `json:"flickr,omitempty"` + Sidecar *Content `json:"sidecar,omitempty"` + Flickr *FlickrMedia `json:"flickr,omitempty"` +} + +// V0SrcSpecific contains source-specific metadata. +type V0SrcSpecific struct { + // TODO: port v0 sources } func (m Content) isZero() bool { diff --git a/meta/meta_test.go b/meta/meta_test.go index c4ffd84..9876949 100644 --- a/meta/meta_test.go +++ b/meta/meta_test.go @@ -4,6 +4,8 @@ import ( "reflect" "testing" "time" + + "github.com/recentralized/structure/index" ) func TestMetaDateCreated(t *testing.T) { @@ -24,25 +26,43 @@ func TestMetaDateCreated(t *testing.T) { want: time.Date(2001, 1, 1, 1, 1, 1, 1, time.UTC), }, { - desc: "sidecar with date", + desc: "oldest of inherent or sidecars", m: &Meta{ - Sidecar: Content{Created: time.Date(2001, 2, 1, 1, 1, 1, 1, time.UTC)}, + Inherent: Content{Created: time.Date(2009, 1, 1, 1, 1, 1, 1, time.UTC)}, + Src: map[index.SrcID]SrcSpecific{ + index.SrcID("a"): { + Sidecar: &Content{Created: time.Date(2004, 1, 1, 1, 1, 1, 1, time.UTC)}, + }, + index.SrcID("b"): { + Sidecar: &Content{Created: time.Date(2001, 1, 1, 1, 1, 1, 1, time.UTC)}, + }, + index.SrcID("c"): { + Sidecar: &Content{Created: time.Date(2003, 1, 1, 1, 1, 1, 1, time.UTC)}, + }, + }, }, - want: time.Date(2001, 2, 1, 1, 1, 1, 1, time.UTC), + want: time.Date(2001, 1, 1, 1, 1, 1, 1, time.UTC), }, { - desc: "prefers sidecar to inherent", + desc: "v0: sidecar with date", m: &Meta{ - Inherent: Content{Created: time.Date(2001, 1, 1, 1, 1, 1, 1, time.UTC)}, - Sidecar: Content{Created: time.Date(2001, 2, 1, 1, 1, 1, 1, time.UTC)}, + V0Sidecar: Content{Created: time.Date(2001, 2, 1, 1, 1, 1, 1, time.UTC)}, }, want: time.Date(2001, 2, 1, 1, 1, 1, 1, time.UTC), }, + { + desc: "v0: prefers sidecar to inherent", + m: &Meta{ + Inherent: Content{Created: time.Date(2001, 1, 1, 1, 1, 1, 1, time.UTC)}, + V0Sidecar: Content{Created: time.Date(2002, 1, 1, 1, 1, 1, 1, time.UTC)}, + }, + want: time.Date(2002, 1, 1, 1, 1, 1, 1, time.UTC), + }, } for _, tt := range tests { got := tt.m.DateCreated() if got, want := got, tt.want; !reflect.DeepEqual(got, want) { - t.Errorf("%q Meta.DateCreated()\ngot %#v\nwant %#v", tt.desc, got, want) + t.Errorf("%q Meta.DateCreated()\ngot %s\nwant %s", tt.desc, got, want) } } }