diff --git a/internal/meta/testdata/apple-test-2.xmp b/internal/meta/testdata/apple-test-2.xmp new file mode 100644 index 000000000..596fde932 --- /dev/null +++ b/internal/meta/testdata/apple-test-2.xmp @@ -0,0 +1,25 @@ + + + + 13.369367199999999 + E + 1 + 52.5250816 + N + 2021-03-26T09:18:59Z + Botanischer Garten + Tulpen am See + + + Krokus + Blume + Schöne Wiese + + + 2021-03-24T13:07:29+01:00 + + + diff --git a/internal/meta/xmp.go b/internal/meta/xmp.go index 9f1072b31..96d05fdbb 100644 --- a/internal/meta/xmp.go +++ b/internal/meta/xmp.go @@ -57,5 +57,13 @@ func (data *Data) XMP(fileName string) (err error) { data.LensModel = doc.LensModel() } + if takenAt := doc.TakenAt(); !takenAt.IsZero() { + data.TakenAt = takenAt + } + + if len(doc.Keywords()) != 0 { + data.AddKeywords(doc.Keywords()) + } + return nil } diff --git a/internal/meta/xmp_document.go b/internal/meta/xmp_document.go index ce23c142a..a8a7a22b8 100644 --- a/internal/meta/xmp_document.go +++ b/internal/meta/xmp_document.go @@ -3,6 +3,8 @@ package meta import ( "encoding/xml" "io/ioutil" + "strings" + "time" ) // XmpDocument represents an XMP sidecar file. @@ -75,6 +77,10 @@ type XmpDocument struct { Text string `xml:",chardata" json:"text,omitempty"` Li []string `xml:"li"` // desk, coffee, computer } `xml:"Bag" json:"bag,omitempty"` + Seq struct { + Text string `xml:",chardata" json:"text,omitempty"` + Li []string `xml:"li"` // desk, coffee, computer + } `xml:"Seq" json:"seq,omitempty"` } `xml:"subject" json:"subject,omitempty"` Rights struct { Text string `xml:",chardata" json:"text,omitempty"` @@ -192,6 +198,7 @@ type XmpDocument struct { } `xml:"RDF" json:"rdf,omitempty"` } +// Load parses an XMP file and populates document values with its contents. func (doc *XmpDocument) Load(filename string) error { data, err := ioutil.ReadFile(filename) @@ -202,30 +209,81 @@ func (doc *XmpDocument) Load(filename string) error { return xml.Unmarshal(data, doc) } +// Title returns the XMP document title. func (doc *XmpDocument) Title() string { - return SanitizeTitle(doc.RDF.Description.Title.Alt.Li.Text) + t := doc.RDF.Description.Title.Alt.Li.Text + t2 := doc.RDF.Description.Title.Text + if t != "" { + return SanitizeTitle(t) + } else if t2 != "" { + return SanitizeTitle(t2) + } + return "" } +// Artist returns the XMP document artist. func (doc *XmpDocument) Artist() string { return SanitizeString(doc.RDF.Description.Creator.Seq.Li) } +// Description returns the XMP document description. func (doc *XmpDocument) Description() string { - return SanitizeDescription(doc.RDF.Description.Description.Alt.Li.Text) + d := doc.RDF.Description.Description.Alt.Li.Text + d2 := doc.RDF.Description.Description.Text + if d != "" { + return SanitizeDescription(d) + } else if d2 != "" { + return SanitizeTitle(d2) + } + return "" } +// Copyright returns the XMP document copyright info. func (doc *XmpDocument) Copyright() string { return SanitizeString(doc.RDF.Description.Rights.Alt.Li.Text) } +// CameraMake returns the XMP document camera make name. func (doc *XmpDocument) CameraMake() string { return SanitizeString(doc.RDF.Description.Make) } +// CameraModel returns the XMP document camera model name. func (doc *XmpDocument) CameraModel() string { return SanitizeString(doc.RDF.Description.Model) } +// LensModel returns the XMP document lens model name. func (doc *XmpDocument) LensModel() string { return SanitizeString(doc.RDF.Description.LensModel) } + +// TakenAt returns the XMP document taken date. +func (doc *XmpDocument) TakenAt() time.Time { + taken := time.Time{} // Unknown + + s := SanitizeString(doc.RDF.Description.DateCreated) + + if s == "" { + return taken + } + + if t, err := time.Parse(time.RFC3339, s); err == nil { + taken = t + } else if t, err := time.Parse("2006-01-02T15:04:05.999999999", s); err == nil { + taken = t + } else if t, err := time.Parse("2006-01-02T15:04:05-07:00", s); err == nil { + taken = t + } else if t, err := time.Parse("2006-01-02T15:04:05", s[:19]); err == nil { + taken = t + } + + return taken +} + +// Keywords returns the XMP document keywords. +func (doc *XmpDocument) Keywords() string { + s := doc.RDF.Description.Subject.Seq.Li + + return strings.Join(s, ", ") +} diff --git a/internal/meta/xmp_test.go b/internal/meta/xmp_test.go index eafe7f54b..ac01cc3fb 100644 --- a/internal/meta/xmp_test.go +++ b/internal/meta/xmp_test.go @@ -2,11 +2,25 @@ package meta import ( "testing" + "time" "github.com/stretchr/testify/assert" ) func TestXMP(t *testing.T) { + t.Run("apple xmp 2", func(t *testing.T) { + data, err := XMP("testdata/apple-test-2.xmp") + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, "Botanischer Garten", data.Title) + assert.Equal(t, time.Date(2021, 3, 24, 13, 07, 29, 0, time.FixedZone("", +3600)), data.TakenAt) + assert.Equal(t, "Tulpen am See", data.Description) + assert.Equal(t, Keywords{"blume", "krokus", "schöne", "wiese"}, data.Keywords) + }) + t.Run("photoshop", func(t *testing.T) { data, err := XMP("testdata/photoshop.xmp") @@ -15,6 +29,8 @@ func TestXMP(t *testing.T) { } assert.Equal(t, "Night Shift / Berlin / 2020", data.Title) + t.Log(data.TakenAt) + assert.Equal(t, time.Date(2020, 1, 1, 17, 28, 25, 729626112, time.UTC), data.TakenAt) assert.Equal(t, "Michael Mayer", data.Artist) assert.Equal(t, "Example file for development", data.Description) assert.Equal(t, "This is an (edited) legal notice", data.Copyright) diff --git a/internal/photoprism/index_related_test.go b/internal/photoprism/index_related_test.go index 6f7947278..eac04f85a 100644 --- a/internal/photoprism/index_related_test.go +++ b/internal/photoprism/index_related_test.go @@ -13,61 +13,129 @@ import ( ) func TestIndexRelated(t *testing.T) { - conf := config.TestConfig() + t.Run("2018-04-12 19_24_49.gif", func(t *testing.T) { + conf := config.TestConfig() - testFile, err := NewMediaFile("testdata/2018-04-12 19_24_49.gif") + testFile, err := NewMediaFile("testdata/2018-04-12 19_24_49.gif") - if err != nil { - t.Fatal(err) - } - - testRelated, err := testFile.RelatedFiles(true) - - if err != nil { - t.Fatal(err) - } - - testToken := rnd.Token(8) - testPath := filepath.Join(conf.OriginalsPath(), testToken) - - for _, f := range testRelated.Files { - dest := filepath.Join(testPath, f.BaseName()) - - if err := f.Copy(dest); err != nil { - t.Fatalf("COPY FAILED: %s", err) + if err != nil { + t.Fatal(err) } - } - mainFile, err := NewMediaFile(filepath.Join(testPath, "2018-04-12 19_24_49.gif")) + testRelated, err := testFile.RelatedFiles(true) - if err != nil { - t.Fatal(err) - } + if err != nil { + t.Fatal(err) + } - related, err := mainFile.RelatedFiles(true) + testToken := rnd.Token(8) + testPath := filepath.Join(conf.OriginalsPath(), testToken) - if err != nil { - t.Fatal(err) - } + for _, f := range testRelated.Files { + dest := filepath.Join(testPath, f.BaseName()) - tf := classify.New(conf.AssetsPath(), conf.DisableTensorFlow()) - nd := nsfw.New(conf.NSFWModelPath()) - convert := NewConvert(conf) + if err := f.Copy(dest); err != nil { + t.Fatalf("copying test file failed: %s", err) + } + } - ind := NewIndex(conf, tf, nd, convert, NewFiles(), NewPhotos()) - opt := IndexOptionsAll() + mainFile, err := NewMediaFile(filepath.Join(testPath, "2018-04-12 19_24_49.gif")) - result := IndexRelated(related, ind, opt) + if err != nil { + t.Fatal(err) + } - assert.False(t, result.Failed()) - assert.False(t, result.Stacked()) - assert.True(t, result.Success()) - assert.Equal(t, IndexAdded, result.Status) + related, err := mainFile.RelatedFiles(true) - if photo, err := query.PhotoByUID(result.PhotoUID); err != nil { - t.Fatal(err) - } else { - assert.Equal(t, "2018-04-12 19:24:49 +0000 UTC", photo.TakenAt.String()) - assert.Equal(t, "name", photo.TakenSrc) - } + if err != nil { + t.Fatal(err) + } + + tf := classify.New(conf.AssetsPath(), conf.DisableTensorFlow()) + nd := nsfw.New(conf.NSFWModelPath()) + convert := NewConvert(conf) + + ind := NewIndex(conf, tf, nd, convert, NewFiles(), NewPhotos()) + opt := IndexOptionsAll() + + result := IndexRelated(related, ind, opt) + + assert.False(t, result.Failed()) + assert.False(t, result.Stacked()) + assert.True(t, result.Success()) + assert.Equal(t, IndexAdded, result.Status) + + if photo, err := query.PhotoByUID(result.PhotoUID); err != nil { + t.Fatal(err) + } else { + assert.Equal(t, "2018-04-12 19:24:49 +0000 UTC", photo.TakenAt.String()) + assert.Equal(t, "name", photo.TakenSrc) + } + }) + + t.Run("apple-test-2.jpg", func(t *testing.T) { + conf := config.TestConfig() + + testFile, err := NewMediaFile("testdata/apple-test-2.jpg") + + if err != nil { + t.Fatal(err) + } + + testRelated, err := testFile.RelatedFiles(true) + + if err != nil { + t.Fatal(err) + } + + testToken := rnd.Token(8) + testPath := filepath.Join(conf.OriginalsPath(), testToken) + + for _, f := range testRelated.Files { + dest := filepath.Join(testPath, f.BaseName()) + + if err := f.Copy(dest); err != nil { + t.Fatalf("copying test file failed: %s", err) + } + } + + mainFile, err := NewMediaFile(filepath.Join(testPath, "apple-test-2.jpg")) + + if err != nil { + t.Fatal(err) + } + + related, err := mainFile.RelatedFiles(true) + + if err != nil { + t.Fatal(err) + } + + tf := classify.New(conf.AssetsPath(), conf.DisableTensorFlow()) + nd := nsfw.New(conf.NSFWModelPath()) + convert := NewConvert(conf) + + ind := NewIndex(conf, tf, nd, convert, NewFiles(), NewPhotos()) + opt := IndexOptionsAll() + + result := IndexRelated(related, ind, opt) + + assert.False(t, result.Failed()) + assert.False(t, result.Stacked()) + assert.True(t, result.Success()) + assert.Equal(t, IndexAdded, result.Status) + + if photo, err := query.PhotoByUID(result.PhotoUID); err != nil { + t.Fatal(err) + } else { + assert.Equal(t, "Botanischer Garten", photo.PhotoTitle) + assert.Equal(t, "Tulpen am See", photo.PhotoDescription) + assert.Contains(t, photo.Details.Keywords, "krokus") + assert.Contains(t, photo.Details.Keywords, "blume") + assert.Contains(t, photo.Details.Keywords, "schöne") + assert.Contains(t, photo.Details.Keywords, "wiese") + assert.Equal(t, "2021-03-24 12:07:29 +0000 UTC", photo.TakenAt.String()) + assert.Equal(t, "xmp", photo.TakenSrc) + } + }) } diff --git a/internal/photoprism/testdata/apple-test-2.jpg b/internal/photoprism/testdata/apple-test-2.jpg new file mode 100644 index 000000000..91536c64b Binary files /dev/null and b/internal/photoprism/testdata/apple-test-2.jpg differ diff --git a/internal/photoprism/testdata/apple-test-2.xmp b/internal/photoprism/testdata/apple-test-2.xmp new file mode 100644 index 000000000..596fde932 --- /dev/null +++ b/internal/photoprism/testdata/apple-test-2.xmp @@ -0,0 +1,25 @@ + + + + 13.369367199999999 + E + 1 + 52.5250816 + N + 2021-03-26T09:18:59Z + Botanischer Garten + Tulpen am See + + + Krokus + Blume + Schöne Wiese + + + 2021-03-24T13:07:29+01:00 + + +