Compare commits
4 commits
develop
...
1151-apple
Author | SHA1 | Date | |
---|---|---|---|
|
8c214a07cd | ||
|
70c28054b5 | ||
|
db889c7841 | ||
|
defe3d5cba |
7 changed files with 247 additions and 47 deletions
25
internal/meta/testdata/apple-test-2.xmp
vendored
Normal file
25
internal/meta/testdata/apple-test-2.xmp
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
|
||||||
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif="http://ns.adobe.com/exif/1.0/"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||||
|
<exif:GPSLongitude>13.369367199999999</exif:GPSLongitude>
|
||||||
|
<exif:GPSLongitudeRef>E</exif:GPSLongitudeRef>
|
||||||
|
<exif:GPSHPositioningError>1</exif:GPSHPositioningError>
|
||||||
|
<exif:GPSLatitude>52.5250816</exif:GPSLatitude>
|
||||||
|
<exif:GPSLatitudeRef>N</exif:GPSLatitudeRef>
|
||||||
|
<exif:GPSTimeStamp>2021-03-26T09:18:59Z</exif:GPSTimeStamp>
|
||||||
|
<dc:title>Botanischer Garten</dc:title>
|
||||||
|
<dc:description>Tulpen am See</dc:description>
|
||||||
|
<dc:subject>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>Krokus</rdf:li>
|
||||||
|
<rdf:li>Blume</rdf:li>
|
||||||
|
<rdf:li>Schöne Wiese</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</dc:subject>
|
||||||
|
<photoshop:DateCreated>2021-03-24T13:07:29+01:00</photoshop:DateCreated>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:RDF>
|
||||||
|
</x:xmpmeta>
|
|
@ -57,5 +57,13 @@ func (data *Data) XMP(fileName string) (err error) {
|
||||||
data.LensModel = doc.LensModel()
|
data.LensModel = doc.LensModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if takenAt := doc.TakenAt(); !takenAt.IsZero() {
|
||||||
|
data.TakenAt = takenAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(doc.Keywords()) != 0 {
|
||||||
|
data.AddKeywords(doc.Keywords())
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package meta
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// XmpDocument represents an XMP sidecar file.
|
// XmpDocument represents an XMP sidecar file.
|
||||||
|
@ -75,6 +77,10 @@ type XmpDocument struct {
|
||||||
Text string `xml:",chardata" json:"text,omitempty"`
|
Text string `xml:",chardata" json:"text,omitempty"`
|
||||||
Li []string `xml:"li"` // desk, coffee, computer
|
Li []string `xml:"li"` // desk, coffee, computer
|
||||||
} `xml:"Bag" json:"bag,omitempty"`
|
} `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"`
|
} `xml:"subject" json:"subject,omitempty"`
|
||||||
Rights struct {
|
Rights struct {
|
||||||
Text string `xml:",chardata" json:"text,omitempty"`
|
Text string `xml:",chardata" json:"text,omitempty"`
|
||||||
|
@ -192,6 +198,7 @@ type XmpDocument struct {
|
||||||
} `xml:"RDF" json:"rdf,omitempty"`
|
} `xml:"RDF" json:"rdf,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load parses an XMP file and populates document values with its contents.
|
||||||
func (doc *XmpDocument) Load(filename string) error {
|
func (doc *XmpDocument) Load(filename string) error {
|
||||||
data, err := ioutil.ReadFile(filename)
|
data, err := ioutil.ReadFile(filename)
|
||||||
|
|
||||||
|
@ -202,30 +209,81 @@ func (doc *XmpDocument) Load(filename string) error {
|
||||||
return xml.Unmarshal(data, doc)
|
return xml.Unmarshal(data, doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title returns the XMP document title.
|
||||||
func (doc *XmpDocument) Title() string {
|
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 {
|
func (doc *XmpDocument) Artist() string {
|
||||||
return SanitizeString(doc.RDF.Description.Creator.Seq.Li)
|
return SanitizeString(doc.RDF.Description.Creator.Seq.Li)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Description returns the XMP document description.
|
||||||
func (doc *XmpDocument) Description() string {
|
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 {
|
func (doc *XmpDocument) Copyright() string {
|
||||||
return SanitizeString(doc.RDF.Description.Rights.Alt.Li.Text)
|
return SanitizeString(doc.RDF.Description.Rights.Alt.Li.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CameraMake returns the XMP document camera make name.
|
||||||
func (doc *XmpDocument) CameraMake() string {
|
func (doc *XmpDocument) CameraMake() string {
|
||||||
return SanitizeString(doc.RDF.Description.Make)
|
return SanitizeString(doc.RDF.Description.Make)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CameraModel returns the XMP document camera model name.
|
||||||
func (doc *XmpDocument) CameraModel() string {
|
func (doc *XmpDocument) CameraModel() string {
|
||||||
return SanitizeString(doc.RDF.Description.Model)
|
return SanitizeString(doc.RDF.Description.Model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LensModel returns the XMP document lens model name.
|
||||||
func (doc *XmpDocument) LensModel() string {
|
func (doc *XmpDocument) LensModel() string {
|
||||||
return SanitizeString(doc.RDF.Description.LensModel)
|
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, ", ")
|
||||||
|
}
|
||||||
|
|
|
@ -2,11 +2,25 @@ package meta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestXMP(t *testing.T) {
|
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) {
|
t.Run("photoshop", func(t *testing.T) {
|
||||||
data, err := XMP("testdata/photoshop.xmp")
|
data, err := XMP("testdata/photoshop.xmp")
|
||||||
|
|
||||||
|
@ -15,6 +29,8 @@ func TestXMP(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "Night Shift / Berlin / 2020", data.Title)
|
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, "Michael Mayer", data.Artist)
|
||||||
assert.Equal(t, "Example file for development", data.Description)
|
assert.Equal(t, "Example file for development", data.Description)
|
||||||
assert.Equal(t, "This is an (edited) legal notice", data.Copyright)
|
assert.Equal(t, "This is an (edited) legal notice", data.Copyright)
|
||||||
|
|
|
@ -13,61 +13,129 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIndexRelated(t *testing.T) {
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
mainFile, err := NewMediaFile(filepath.Join(testPath, "2018-04-12 19_24_49.gif"))
|
testRelated, err := testFile.RelatedFiles(true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
related, err := mainFile.RelatedFiles(true)
|
testToken := rnd.Token(8)
|
||||||
|
testPath := filepath.Join(conf.OriginalsPath(), testToken)
|
||||||
|
|
||||||
if err != nil {
|
for _, f := range testRelated.Files {
|
||||||
t.Fatal(err)
|
dest := filepath.Join(testPath, f.BaseName())
|
||||||
}
|
|
||||||
|
|
||||||
tf := classify.New(conf.AssetsPath(), conf.DisableTensorFlow())
|
if err := f.Copy(dest); err != nil {
|
||||||
nd := nsfw.New(conf.NSFWModelPath())
|
t.Fatalf("copying test file failed: %s", err)
|
||||||
convert := NewConvert(conf)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ind := NewIndex(conf, tf, nd, convert, NewFiles(), NewPhotos())
|
mainFile, err := NewMediaFile(filepath.Join(testPath, "2018-04-12 19_24_49.gif"))
|
||||||
opt := IndexOptionsAll()
|
|
||||||
|
|
||||||
result := IndexRelated(related, ind, opt)
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
assert.False(t, result.Failed())
|
related, err := mainFile.RelatedFiles(true)
|
||||||
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
}
|
||||||
assert.Equal(t, "2018-04-12 19:24:49 +0000 UTC", photo.TakenAt.String())
|
|
||||||
assert.Equal(t, "name", photo.TakenSrc)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
BIN
internal/photoprism/testdata/apple-test-2.jpg
vendored
Normal file
BIN
internal/photoprism/testdata/apple-test-2.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.6 MiB |
25
internal/photoprism/testdata/apple-test-2.xmp
vendored
Normal file
25
internal/photoprism/testdata/apple-test-2.xmp
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
|
||||||
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif="http://ns.adobe.com/exif/1.0/"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||||
|
<exif:GPSLongitude>13.369367199999999</exif:GPSLongitude>
|
||||||
|
<exif:GPSLongitudeRef>E</exif:GPSLongitudeRef>
|
||||||
|
<exif:GPSHPositioningError>1</exif:GPSHPositioningError>
|
||||||
|
<exif:GPSLatitude>52.5250816</exif:GPSLatitude>
|
||||||
|
<exif:GPSLatitudeRef>N</exif:GPSLatitudeRef>
|
||||||
|
<exif:GPSTimeStamp>2021-03-26T09:18:59Z</exif:GPSTimeStamp>
|
||||||
|
<dc:title>Botanischer Garten</dc:title>
|
||||||
|
<dc:description>Tulpen am See</dc:description>
|
||||||
|
<dc:subject>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>Krokus</rdf:li>
|
||||||
|
<rdf:li>Blume</rdf:li>
|
||||||
|
<rdf:li>Schöne Wiese</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</dc:subject>
|
||||||
|
<photoshop:DateCreated>2021-03-24T13:07:29+01:00</photoshop:DateCreated>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:RDF>
|
||||||
|
</x:xmpmeta>
|
Loading…
Reference in a new issue