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
283 changes: 193 additions & 90 deletions protogen/gen/opencloud/messages/search/v0/search.pb.go

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions protogen/gen/opencloud/services/search/v0/search.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,9 @@
"type": "string"
}
},
"motionPhoto": {
"$ref": "#/definitions/v0MotionPhoto"
},
"video": {
"$ref": "#/definitions/v0Video"
}
Expand Down Expand Up @@ -393,6 +396,23 @@
}
}
},
"v0MotionPhoto": {
"type": "object",
"properties": {
"version": {
"type": "integer",
"format": "int32"
},
"presentationTimestampUs": {
"type": "string",
"format": "int64"
},
"videoSize": {
"type": "string",
"format": "int64"
}
}
},
"v0Photo": {
"type": "object",
"properties": {
Expand Down
7 changes: 7 additions & 0 deletions protogen/proto/opencloud/messages/search/v0/search.proto
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ message Video {
optional int32 width = 10;
}

message MotionPhoto {
optional int32 version = 1;
optional int64 presentationTimestampUs = 2;
optional int64 videoSize = 3;
}

message Entity {
Reference ref = 1;
ResourceID id = 2;
Expand All @@ -93,6 +99,7 @@ message Entity {
Image image = 18;
Photo photo = 19;
repeated string favorites = 20;
MotionPhoto motionPhoto = 21;
Video video = 22;
}

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 @@ -485,6 +485,7 @@ func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *sto
driveItem.Location = metadataToFacet[libregraph.GeoCoordinates](metadata, "location")
driveItem.Photo = metadataToFacet[libregraph.Photo](metadata, "photo")
driveItem.Video = metadataToFacet[libregraph.Video](metadata, "video")
driveItem.LibreGraphMotionPhoto = metadataToFacet[libregraph.MotionPhoto](metadata, "motionPhoto")
driveItem.LibreGraphMeFollowing = libregraph.PtrBool(metadata[_favoriteMetadataKey] == "1")
if t := metadata["tags"]; t != "" {
driveItem.LibreGraphTags = tags.New(t).AsSlice()
Expand Down
31 changes: 16 additions & 15 deletions services/search/pkg/bleve/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,21 +125,22 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques
ResourceId: resourceIDtoSearchID(rootID),
Path: getFieldValue[string](hit.Fields, "Path"),
},
Id: resourceIDtoSearchID(rID),
Name: getFieldValue[string](hit.Fields, "Name"),
ParentId: resourceIDtoSearchID(pID),
Size: uint64(getFieldValue[float64](hit.Fields, "Size")),
Type: uint64(getFieldValue[float64](hit.Fields, "Type")),
MimeType: getFieldValue[string](hit.Fields, "MimeType"),
Deleted: getFieldValue[bool](hit.Fields, "Deleted"),
Tags: getFieldSliceValue[string](hit.Fields, "Tags"),
Favorites: getFieldSliceValue[string](hit.Fields, "Favorites"),
Highlights: getFragmentValue(hit.Fragments, "Content", 0),
Audio: hitToFacet[searchMessage.Audio](hit.Fields, "audio"),
Image: hitToFacet[searchMessage.Image](hit.Fields, "image"),
Location: hitToFacet[searchMessage.GeoCoordinates](hit.Fields, "location"),
Photo: hitToFacet[searchMessage.Photo](hit.Fields, "photo"),
Video: hitToFacet[searchMessage.Video](hit.Fields, "video"),
Id: resourceIDtoSearchID(rID),
Name: getFieldValue[string](hit.Fields, "Name"),
ParentId: resourceIDtoSearchID(pID),
Size: uint64(getFieldValue[float64](hit.Fields, "Size")),
Type: uint64(getFieldValue[float64](hit.Fields, "Type")),
MimeType: getFieldValue[string](hit.Fields, "MimeType"),
Deleted: getFieldValue[bool](hit.Fields, "Deleted"),
Tags: getFieldSliceValue[string](hit.Fields, "Tags"),
Favorites: getFieldSliceValue[string](hit.Fields, "Favorites"),
Highlights: getFragmentValue(hit.Fragments, "Content", 0),
Audio: hitToFacet[searchMessage.Audio](hit.Fields, "audio"),
Image: hitToFacet[searchMessage.Image](hit.Fields, "image"),
Location: hitToFacet[searchMessage.GeoCoordinates](hit.Fields, "location"),
Photo: hitToFacet[searchMessage.Photo](hit.Fields, "photo"),
Video: hitToFacet[searchMessage.Video](hit.Fields, "video"),
MotionPhoto: hitToFacet[searchMessage.MotionPhoto](hit.Fields, "motionPhoto"),
},
}

Expand Down
45 changes: 45 additions & 0 deletions services/search/pkg/bleve/testdata/mapping.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,51 @@
}
]
},
"motionPhoto": {
"enabled": true,
"dynamic": true,
"properties": {
"presentationTimestampUs": {
"enabled": true,
"dynamic": true,
"fields": [
{
"type": "number",
"store": true,
"index": true,
"include_in_all": true,
"docvalues": true
}
]
},
"version": {
"enabled": true,
"dynamic": true,
"fields": [
{
"type": "number",
"store": true,
"index": true,
"include_in_all": true,
"docvalues": true
}
]
},
"videoSize": {
"enabled": true,
"dynamic": true,
"fields": [
{
"type": "number",
"store": true,
"index": true,
"include_in_all": true,
"docvalues": true
}
]
}
}
},
"photo": {
"enabled": true,
"dynamic": true,
Expand Down
27 changes: 14 additions & 13 deletions services/search/pkg/content/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,20 @@ func init() {
// Document wraps all resource meta fields,
// it is used as a content extraction result.
type Document struct {
Title string `json:"Title"`
Name string `json:"Name"`
Content string `json:"Content"`
Size uint64 `json:"Size"`
Mtime *time.Time `json:"Mtime,omitempty"`
MimeType string `json:"MimeType"`
Tags []string `json:"Tags"`
Favorites []string `json:"Favorites"`
Audio *libregraph.Audio `json:"audio,omitempty"`
Image *libregraph.Image `json:"image,omitempty"`
Location *libregraph.GeoCoordinates `json:"location,omitempty"`
Photo *libregraph.Photo `json:"photo,omitempty"`
Video *libregraph.Video `json:"video,omitempty"`
Title string `json:"Title"`
Name string `json:"Name"`
Content string `json:"Content"`
Size uint64 `json:"Size"`
Mtime *time.Time `json:"Mtime,omitempty"`
MimeType string `json:"MimeType"`
Tags []string `json:"Tags"`
Favorites []string `json:"Favorites"`
Audio *libregraph.Audio `json:"audio,omitempty"`
Image *libregraph.Image `json:"image,omitempty"`
Location *libregraph.GeoCoordinates `json:"location,omitempty"`
Photo *libregraph.Photo `json:"photo,omitempty"`
Video *libregraph.Video `json:"video,omitempty"`
MotionPhoto *libregraph.MotionPhoto `json:"motionPhoto,omitempty"`
}

func CleanString(content, langCode string) string {
Expand Down
23 changes: 8 additions & 15 deletions services/search/pkg/content/extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package content

import (
"context"
"errors"
"fmt"

provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
Expand All @@ -13,19 +12,13 @@ type Extractor interface {
Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, error)
}

func getFirstValue(m map[string][]string, key string) (string, error) {
if m == nil {
return "", errors.New("undefined map")
// getFirstValue returns the first metadata value present among keys, trying them
// in order. It errors when the map is nil or none of the keys holds a value.
func getFirstValue(m map[string][]string, keys ...string) (string, error) {
for _, key := range keys {
if v, ok := m[key]; ok && len(v) > 0 {
return v[0], nil
}
}

v, ok := m[key]
if !ok {
return "", fmt.Errorf("unknown key: %v", key)
}

if len(v) == 0 {
return "", fmt.Errorf("no values for: %v", key)
}

return v[0], nil
return "", fmt.Errorf("no value for keys: %v", keys)
}
18 changes: 16 additions & 2 deletions services/search/pkg/content/tika.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"slices"
"strings"

gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
Expand Down Expand Up @@ -112,8 +113,21 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document,
if v := t.getAudio(meta); v != nil {
doc.Audio = v
}
if v := t.getVideo(meta); v != nil {
doc.Video = v
}

if len(metas) > 0 {
// the video facet says the file is a video, so it comes from the file
// itself: the clip tika extracts from a motion photo must not make its
// image look like one
doc.Video = t.getVideo(metas[0])
}

// a motion photo is the xmp on the file itself plus the video tika extracted
// from it. The xmp alone proves nothing: a share can keep it and strip the
// appended video.
if len(metas) > 0 {
if i := slices.IndexFunc(metas[1:], isVideo); i >= 0 {
doc.MotionPhoto = t.getMotionPhoto(metas[0], metas[i+1])
}
}

Expand Down
63 changes: 63 additions & 0 deletions services/search/pkg/content/tika_motion_photo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package content

import (
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"strconv"
"strings"
)

// getMotionPhoto reads Google Motion Photo XMP, which Tika exposes under the
// canonical Camera/Container prefixes. It covers both the current MotionPhoto
// scheme and the legacy MicroVideo scheme. videoSize (needed to range-fetch the
// video) comes from the video tika extracted, and is required: without it the
// facet is dropped.
func (t Tika) getMotionPhoto(meta, video map[string][]string) *libregraph.MotionPhoto {
// the marker is what makes this a motion photo rather than a picture that
// happens to carry a video: per the spec only a value of 1 counts, every
// other value means "treat as a still image".
if v, err := getFirstValue(meta, "Camera:MotionPhoto", "Camera:MicroVideo"); err != nil || v != "1" {
return nil
}

var motionPhoto *libregraph.MotionPhoto
initMotionPhoto := func() {
if motionPhoto == nil {
motionPhoto = libregraph.NewMotionPhoto()
}
}

if v, err := getFirstValue(meta, "Camera:MotionPhotoVersion", "Camera:MicroVideoVersion"); err == nil {
if i, err := strconv.ParseInt(v, 10, 32); err == nil {
initMotionPhoto()
motionPhoto.SetVersion(int32(i))
}
}

if v, err := getFirstValue(meta, "Camera:MotionPhotoPresentationTimestampUs", "Camera:MicroVideoPresentationTimestampUs"); err == nil {
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
initMotionPhoto()
motionPhoto.SetPresentationTimestampUs(i)
}
}

if v, err := getFirstValue(video, "Content-Length"); err == nil {
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
initMotionPhoto()
motionPhoto.SetVideoSize(i)
}
}

if motionPhoto == nil || !motionPhoto.HasVideoSize() {
return nil
}
return motionPhoto
}

// isVideo reports whether meta describes a video. Tika emits the video appended
// to a motion photo as an embedded document, and it only does so when the bytes
// the xmp advertises are really there: a shared motion photo can keep the xmp
// and lose the video.
func isVideo(meta map[string][]string) bool {
v, err := getFirstValue(meta, "Content-Type")
return err == nil && strings.HasPrefix(v, "video/")
}
Loading