Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 220 additions & 93 deletions protogen/gen/opencloud/messages/search/v0/search.pb.go

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions protogen/gen/opencloud/services/search/v0/search.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@
},
"video": {
"$ref": "#/definitions/v0Video"
},
"livePhoto": {
"$ref": "#/definitions/v0LivePhoto"
}
}
},
Expand Down Expand Up @@ -382,6 +385,29 @@
}
}
},
"v0LivePhoto": {
"type": "object",
"properties": {
"contentId": {
"type": "string"
},
"stillImageTimeUs": {
"type": "string",
"format": "int64"
},
"auto": {
"type": "boolean"
},
"vitalityScore": {
"type": "number",
"format": "double"
},
"vitalityScoringVersion": {
"type": "string",
"format": "int64"
}
}
},
"v0Match": {
"type": "object",
"properties": {
Expand Down
9 changes: 9 additions & 0 deletions protogen/proto/opencloud/messages/search/v0/search.proto
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ message MotionPhoto {
optional int64 videoSize = 3;
}

message LivePhoto {
optional string contentId = 1;
optional int64 stillImageTimeUs = 2;
optional bool auto = 3;
optional double vitalityScore = 4;
optional int64 vitalityScoringVersion = 5;
}

message Entity {
Reference ref = 1;
ResourceID id = 2;
Expand All @@ -101,6 +109,7 @@ message Entity {
repeated string favorites = 20;
MotionPhoto motionPhoto = 21;
Video video = 22;
LivePhoto livePhoto = 23;
}

message Match {
Expand Down
1 change: 1 addition & 0 deletions services/graph/pkg/service/v0/driveitems.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *sto
driveItem.Photo = metadataToFacet[libregraph.Photo](metadata, "photo")
driveItem.Video = metadataToFacet[libregraph.Video](metadata, "video")
driveItem.LibreGraphMotionPhoto = metadataToFacet[libregraph.MotionPhoto](metadata, "motionPhoto")
driveItem.LibreGraphLivePhoto = metadataToFacet[libregraph.LivePhoto](metadata, "livePhoto")
driveItem.LibreGraphMeFollowing = libregraph.PtrBool(metadata[_favoriteMetadataKey] == "1")
if t := metadata["tags"]; t != "" {
driveItem.LibreGraphTags = tags.New(t).AsSlice()
Expand Down
1 change: 1 addition & 0 deletions services/search/pkg/bleve/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques
Photo: hitToFacet[searchMessage.Photo](hit.Fields, "photo"),
Video: hitToFacet[searchMessage.Video](hit.Fields, "video"),
MotionPhoto: hitToFacet[searchMessage.MotionPhoto](hit.Fields, "motionPhoto"),
LivePhoto: hitToFacet[searchMessage.LivePhoto](hit.Fields, "livePhoto"),
},
}

Expand Down
73 changes: 73 additions & 0 deletions services/search/pkg/bleve/testdata/mapping.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,79 @@
}
}
},
"livePhoto": {
"enabled": true,
"dynamic": true,
"properties": {
"auto": {
"enabled": true,
"dynamic": true,
"fields": [
{
"type": "boolean",
"store": true,
"index": true,
"include_in_all": true,
"docvalues": true
}
]
},
"contentId": {
"enabled": true,
"dynamic": true,
"fields": [
{
"type": "text",
"analyzer": "keyword",
"store": true,
"index": true,
"include_term_vectors": true,
"include_in_all": true,
"docvalues": true
}
]
},
"stillImageTimeUs": {
"enabled": true,
"dynamic": true,
"fields": [
{
"type": "number",
"store": true,
"index": true,
"include_in_all": true,
"docvalues": true
}
]
},
"vitalityScore": {
"enabled": true,
"dynamic": true,
"fields": [
{
"type": "number",
"store": true,
"index": true,
"include_in_all": true,
"docvalues": true
}
]
},
"vitalityScoringVersion": {
"enabled": true,
"dynamic": true,
"fields": [
{
"type": "number",
"store": true,
"index": true,
"include_in_all": true,
"docvalues": true
}
]
}
}
},
"location": {
"enabled": true,
"dynamic": true,
Expand Down
1 change: 1 addition & 0 deletions services/search/pkg/content/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Document struct {
Photo *libregraph.Photo `json:"photo,omitempty"`
Video *libregraph.Video `json:"video,omitempty"`
MotionPhoto *libregraph.MotionPhoto `json:"motionPhoto,omitempty"`
LivePhoto *libregraph.LivePhoto `json:"livePhoto,omitempty"`
}

func CleanString(content, langCode string) string {
Expand Down
3 changes: 3 additions & 0 deletions services/search/pkg/content/tika.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document,
if v := t.getAudio(meta); v != nil {
doc.Audio = v
}
if v := t.getLivePhoto(meta); v != nil {
doc.LivePhoto = v
}
}

if len(metas) > 0 {
Expand Down
47 changes: 47 additions & 0 deletions services/search/pkg/content/tika_live_photo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package content

import (
"math"
"strconv"

libregraph "github.com/opencloud-eu/libre-graph-api-go"
)

func (t Tika) getLivePhoto(meta map[string][]string) *libregraph.LivePhoto {
// ContentId pairs the two halves, without it there is no live photo. The
// video carries it in the QuickTime item list, the still image in the Apple
// maker note, which tika surfaces as "Content Identifier". A file is only
// ever one half, so both keys are read.
contentID, err := getFirstValue(meta, "com.apple.quicktime.content.identifier", "Content Identifier")
if err != nil || contentID == "" {
return nil
}
livePhoto := libregraph.NewLivePhoto(contentID)

// tika emits still-image-time already in microseconds
if v, err := getFirstValue(meta, "quicktime:still-image-time"); err == nil {
if f, err := strconv.ParseFloat(v, 64); err == nil {
livePhoto.SetStillImageTimeUs(int64(math.Round(f)))
}
}

if v, err := getFirstValue(meta, "com.apple.quicktime.live-photo.auto"); err == nil {
if b, err := strconv.ParseBool(v); err == nil {
livePhoto.SetAuto(b)
}
}

if v, err := getFirstValue(meta, "com.apple.quicktime.live-photo.vitality-score"); err == nil {
if f, err := strconv.ParseFloat(v, 64); err == nil {
livePhoto.SetVitalityScore(f)
}
}

if v, err := getFirstValue(meta, "com.apple.quicktime.live-photo.vitality-scoring-version"); err == nil {
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
livePhoto.SetVitalityScoringVersion(i)
}
}

return livePhoto
}
77 changes: 77 additions & 0 deletions services/search/pkg/content/tika_live_photo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package content

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)

var _ = Describe("getLivePhoto", func() {
It("maps the video half of a live photo", func() {
livePhoto := Tika{}.getLivePhoto(map[string][]string{
"Content-Type": {"video/quicktime"},
"com.apple.quicktime.content.identifier": {"6F1A2B3C-1234-4E5F-9A8B-0011223344CC"},
"quicktime:still-image-time": {"1500000"},
"com.apple.quicktime.live-photo.auto": {"1"},
"com.apple.quicktime.live-photo.vitality-score": {"0.75"},
"com.apple.quicktime.live-photo.vitality-scoring-version": {"4"},
})
Expect(livePhoto).ToNot(BeNil())

Expect(livePhoto.ContentId).To(Equal("6F1A2B3C-1234-4E5F-9A8B-0011223344CC"))
Expect(livePhoto.StillImageTimeUs).To(Equal(libregraph.PtrInt64(1500000)))
Expect(livePhoto.Auto).To(Equal(libregraph.PtrBool(true)))
Expect(livePhoto.VitalityScore).To(Equal(libregraph.PtrFloat64(0.75)))
Expect(livePhoto.VitalityScoringVersion).To(Equal(libregraph.PtrInt64(4)))
})

It("maps the still half via the Apple maker-note content identifier", func() {
livePhoto := Tika{}.getLivePhoto(map[string][]string{
"Content-Type": {"image/heic"},
"Content Identifier": {"6F1A2B3C-1234-4E5F-9A8B-0011223344CC"},
})
Expect(livePhoto).ToNot(BeNil())
Expect(livePhoto.ContentId).To(Equal("6F1A2B3C-1234-4E5F-9A8B-0011223344CC"))
})

It("returns nil without a content identifier", func() {
Expect(Tika{}.getLivePhoto(map[string][]string{
"Content-Type": {"image/jpeg"},
})).To(BeNil())
Expect(Tika{}.getLivePhoto(map[string][]string{
"Content-Type": {"image/heic"},
"Content Identifier": {""},
})).To(BeNil(), "an empty pairing id is no live photo")
})

It("rounds a fractional still-image-time", func() {
livePhoto := Tika{}.getLivePhoto(map[string][]string{
"com.apple.quicktime.content.identifier": {"6F1A2B3C"},
"quicktime:still-image-time": {"1500000.7"},
})
Expect(livePhoto).ToNot(BeNil())
Expect(livePhoto.StillImageTimeUs).To(Equal(libregraph.PtrInt64(1500001)))
})

It("keeps the facet when optional values are malformed", func() {
livePhoto := Tika{}.getLivePhoto(map[string][]string{
"com.apple.quicktime.content.identifier": {"6F1A2B3C"},
"com.apple.quicktime.live-photo.auto": {"maybe"},
"com.apple.quicktime.live-photo.vitality-score": {"very"},
})
Expect(livePhoto).ToNot(BeNil())
Expect(livePhoto.Auto).To(BeNil())
Expect(livePhoto.VitalityScore).To(BeNil())
})

It("leaves the video-only fields empty on the still half", func() {
livePhoto := Tika{}.getLivePhoto(map[string][]string{
"Content Identifier": {"6F1A2B3C"},
})
Expect(livePhoto).ToNot(BeNil())
Expect(livePhoto.StillImageTimeUs).To(BeNil())
Expect(livePhoto.Auto).To(BeNil())
Expect(livePhoto.VitalityScore).To(BeNil())
Expect(livePhoto.VitalityScoringVersion).To(BeNil())
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func OpenSearchHitToMatch(hit opensearchgoAPI.SearchHit) (*searchMessage.Match,
Photo: copyFacet[searchMessage.Photo](resource.Photo),
Video: copyFacet[searchMessage.Video](resource.Video),
MotionPhoto: copyFacet[searchMessage.MotionPhoto](resource.MotionPhoto),
LivePhoto: copyFacet[searchMessage.LivePhoto](resource.LivePhoto),
},
}

Expand Down
19 changes: 19 additions & 0 deletions services/search/pkg/opensearch/testdata/resource.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,25 @@
}
}
},
"livePhoto": {
"properties": {
"auto": {
"type": "boolean"
},
"contentId": {
"type": "keyword"
},
"stillImageTimeUs": {
"type": "long"
},
"vitalityScore": {
"type": "double"
},
"vitalityScoringVersion": {
"type": "long"
}
}
},
"location": {
"properties": {
"altitude": {
Expand Down
19 changes: 10 additions & 9 deletions services/search/pkg/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,16 @@ var resourceFieldOverrides = sync.OnceValue(func() map[string]mapping.FieldOpts
// every keyword field searches case-insensitively and by word (name,
// title, the facets) unless opted out: ids are opaque, paths are POSIX,
// the mime type is normalized already, a tag is one label
"ID": {CaseInsensitive: &False, NoWordBreaker: &True},
"RootID": {CaseInsensitive: &False, NoWordBreaker: &True},
"ParentID": {CaseInsensitive: &False, NoWordBreaker: &True},
"Path": {Type: mapping.TypePath, CaseInsensitive: &False},
"MimeType": {CaseInsensitive: &False, NoWordBreaker: &True},
"Content": {Type: mapping.TypeFulltext},
"Tags": {NoWordBreaker: &True, IncludeInAll: &False},
"Favorites": {NoWordBreaker: &True, IncludeInAll: &False, CaseInsensitive: &False}, // opaque user ids
"location": {Type: mapping.TypeGeopoint},
"ID": {CaseInsensitive: &False, NoWordBreaker: &True},
"RootID": {CaseInsensitive: &False, NoWordBreaker: &True},
"ParentID": {CaseInsensitive: &False, NoWordBreaker: &True},
"Path": {Type: mapping.TypePath, CaseInsensitive: &False},
"MimeType": {CaseInsensitive: &False, NoWordBreaker: &True},
"Content": {Type: mapping.TypeFulltext},
"Tags": {NoWordBreaker: &True, IncludeInAll: &False},
"Favorites": {NoWordBreaker: &True, IncludeInAll: &False, CaseInsensitive: &False}, // opaque user ids
"livePhoto.contentId": {NoWordBreaker: &True, CaseInsensitive: &False}, // opaque pairing uuid
"location": {Type: mapping.TypeGeopoint},
}
})

Expand Down
1 change: 1 addition & 0 deletions services/search/pkg/search/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ func (s *Service) doUpsertItem(ref *provider.Reference, batch BatchOperator) {
facetToMetadata(metadata, doc.Photo, "libre.graph.photo.")
facetToMetadata(metadata, doc.Video, "libre.graph.video.")
facetToMetadata(metadata, doc.MotionPhoto, "libre.graph.motionPhoto.")
facetToMetadata(metadata, doc.LivePhoto, "libre.graph.livePhoto.")
if len(metadata) == 0 {
return
}
Expand Down