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
+
+
+