diff --git a/pkg/conversions/strings.go b/pkg/conversions/strings.go index 63cc68f2a7..a03b7d963b 100644 --- a/pkg/conversions/strings.go +++ b/pkg/conversions/strings.go @@ -1,6 +1,7 @@ package conversions import ( + "strconv" "strings" ) @@ -14,3 +15,12 @@ func StringToSliceString(src string, sep string) []string { return parts } + +// StringToInt32 parses s as a base-10 int32, returning fallback on any error. +func StringToInt32(s string, fallback int32) int32 { + v, err := strconv.ParseInt(s, 10, 32) + if err != nil { + return fallback + } + return int32(v) +} diff --git a/protogen/gen/opencloud/messages/search/v0/search.pb.go b/protogen/gen/opencloud/messages/search/v0/search.pb.go index e5c74946ea..6f23518834 100644 --- a/protogen/gen/opencloud/messages/search/v0/search.pb.go +++ b/protogen/gen/opencloud/messages/search/v0/search.pb.go @@ -361,6 +361,63 @@ func (x *Image) GetHeight() int32 { return 0 } +// Preview carries the dimensions of an embedded preview (e.g. audio cover art). +// Its presence signals that a preview is available for the resource. +type Preview struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Width *int32 `protobuf:"varint,1,opt,name=width,proto3,oneof" json:"width,omitempty"` + Height *int32 `protobuf:"varint,2,opt,name=height,proto3,oneof" json:"height,omitempty"` +} + +func (x *Preview) Reset() { + *x = Preview{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Preview) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Preview) ProtoMessage() {} + +func (x *Preview) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Preview.ProtoReflect.Descriptor instead. +func (*Preview) Descriptor() ([]byte, []int) { + return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{4} +} + +func (x *Preview) GetWidth() int32 { + if x != nil && x.Width != nil { + return *x.Width + } + return 0 +} + +func (x *Preview) GetHeight() int32 { + if x != nil && x.Height != nil { + return *x.Height + } + return 0 +} + type GeoCoordinates struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -374,7 +431,7 @@ type GeoCoordinates struct { func (x *GeoCoordinates) Reset() { *x = GeoCoordinates{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[4] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -387,7 +444,7 @@ func (x *GeoCoordinates) String() string { func (*GeoCoordinates) ProtoMessage() {} func (x *GeoCoordinates) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[4] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -400,7 +457,7 @@ func (x *GeoCoordinates) ProtoReflect() protoreflect.Message { // Deprecated: Use GeoCoordinates.ProtoReflect.Descriptor instead. func (*GeoCoordinates) Descriptor() ([]byte, []int) { - return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{4} + return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{5} } func (x *GeoCoordinates) GetAltitude() float64 { @@ -443,7 +500,7 @@ type Photo struct { func (x *Photo) Reset() { *x = Photo{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[5] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -456,7 +513,7 @@ func (x *Photo) String() string { func (*Photo) ProtoMessage() {} func (x *Photo) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[5] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -469,7 +526,7 @@ func (x *Photo) ProtoReflect() protoreflect.Message { // Deprecated: Use Photo.ProtoReflect.Descriptor instead. func (*Photo) Descriptor() ([]byte, []int) { - return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{5} + return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{6} } func (x *Photo) GetCameraMake() string { @@ -560,12 +617,13 @@ type Entity struct { Image *Image `protobuf:"bytes,18,opt,name=image,proto3" json:"image,omitempty"` Photo *Photo `protobuf:"bytes,19,opt,name=photo,proto3" json:"photo,omitempty"` Favorites []string `protobuf:"bytes,20,rep,name=favorites,proto3" json:"favorites,omitempty"` + Preview *Preview `protobuf:"bytes,21,opt,name=preview,proto3" json:"preview,omitempty"` } func (x *Entity) Reset() { *x = Entity{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[6] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -578,7 +636,7 @@ func (x *Entity) String() string { func (*Entity) ProtoMessage() {} func (x *Entity) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[6] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -591,7 +649,7 @@ func (x *Entity) ProtoReflect() protoreflect.Message { // Deprecated: Use Entity.ProtoReflect.Descriptor instead. func (*Entity) Descriptor() ([]byte, []int) { - return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{6} + return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{7} } func (x *Entity) GetRef() *Reference { @@ -734,6 +792,13 @@ func (x *Entity) GetFavorites() []string { return nil } +func (x *Entity) GetPreview() *Preview { + if x != nil { + return x.Preview + } + return nil +} + type Match struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -748,7 +813,7 @@ type Match struct { func (x *Match) Reset() { *x = Match{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[7] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -761,7 +826,7 @@ func (x *Match) String() string { func (*Match) ProtoMessage() {} func (x *Match) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[7] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -774,7 +839,7 @@ func (x *Match) ProtoReflect() protoreflect.Message { // Deprecated: Use Match.ProtoReflect.Descriptor instead. func (*Match) Descriptor() ([]byte, []int) { - return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{7} + return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{8} } func (x *Match) GetEntity() *Entity { @@ -864,118 +929,127 @@ var file_opencloud_messages_search_v0_search_proto_rawDesc = []byte{ 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x48, 0x01, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x77, 0x69, 0x64, 0x74, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x68, 0x65, 0x69, - 0x67, 0x68, 0x74, 0x22, 0x9d, 0x01, 0x0a, 0x0e, 0x47, 0x65, 0x6f, 0x43, 0x6f, 0x6f, 0x72, 0x64, - 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x08, 0x61, 0x6c, 0x74, 0x69, 0x74, 0x75, - 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x08, 0x61, 0x6c, 0x74, 0x69, - 0x74, 0x75, 0x64, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x6c, 0x61, 0x74, 0x69, 0x74, - 0x75, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x48, 0x01, 0x52, 0x08, 0x6c, 0x61, 0x74, - 0x69, 0x74, 0x75, 0x64, 0x65, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x6c, 0x6f, 0x6e, 0x67, - 0x69, 0x74, 0x75, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x48, 0x02, 0x52, 0x09, 0x6c, - 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, - 0x61, 0x6c, 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6c, 0x61, 0x74, - 0x69, 0x74, 0x75, 0x64, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x69, 0x74, - 0x75, 0x64, 0x65, 0x22, 0x9b, 0x04, 0x0a, 0x05, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x12, 0x23, 0x0a, - 0x0a, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x4d, 0x61, 0x6b, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x0a, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x4d, 0x61, 0x6b, 0x65, 0x88, - 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0b, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x4d, 0x6f, 0x64, 0x65, - 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0b, 0x63, 0x61, 0x6d, 0x65, 0x72, - 0x61, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x13, 0x65, 0x78, 0x70, - 0x6f, 0x73, 0x75, 0x72, 0x65, 0x44, 0x65, 0x6e, 0x6f, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x48, 0x02, 0x52, 0x13, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x75, - 0x72, 0x65, 0x44, 0x65, 0x6e, 0x6f, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, - 0x12, 0x31, 0x0a, 0x11, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x75, 0x72, 0x65, 0x4e, 0x75, 0x6d, 0x65, - 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x02, 0x48, 0x03, 0x52, 0x11, 0x65, - 0x78, 0x70, 0x6f, 0x73, 0x75, 0x72, 0x65, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, - 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x66, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x02, 0x48, 0x04, 0x52, 0x07, 0x66, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x88, - 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0b, 0x66, 0x6f, 0x63, 0x61, 0x6c, 0x4c, 0x65, 0x6e, 0x67, 0x74, - 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x02, 0x48, 0x05, 0x52, 0x0b, 0x66, 0x6f, 0x63, 0x61, 0x6c, - 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x15, 0x0a, 0x03, 0x69, 0x73, 0x6f, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x06, 0x52, 0x03, 0x69, 0x73, 0x6f, 0x88, 0x01, 0x01, - 0x12, 0x25, 0x0a, 0x0b, 0x6f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x05, 0x48, 0x07, 0x52, 0x0b, 0x6f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, 0x0d, 0x74, 0x61, 0x6b, 0x65, 0x6e, - 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x08, 0x52, 0x0d, 0x74, 0x61, - 0x6b, 0x65, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0d, - 0x0a, 0x0b, 0x5f, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x4d, 0x61, 0x6b, 0x65, 0x42, 0x0e, 0x0a, - 0x0c, 0x5f, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x42, 0x16, 0x0a, - 0x14, 0x5f, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x75, 0x72, 0x65, 0x44, 0x65, 0x6e, 0x6f, 0x6d, 0x69, - 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x75, - 0x72, 0x65, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x42, 0x0a, 0x0a, 0x08, 0x5f, - 0x66, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x66, 0x6f, 0x63, 0x61, - 0x6c, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x69, 0x73, 0x6f, 0x42, - 0x0e, 0x0a, 0x0c, 0x5f, 0x6f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, - 0x10, 0x0a, 0x0e, 0x5f, 0x74, 0x61, 0x6b, 0x65, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, - 0x65, 0x22, 0xfa, 0x06, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x03, - 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x70, 0x65, 0x6e, - 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, - 0x63, 0x65, 0x52, 0x03, 0x72, 0x65, 0x66, 0x12, 0x38, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, - 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x65, 0x74, 0x61, 0x67, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x65, 0x74, 0x61, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x48, 0x0a, - 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, - 0x69, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x65, 0x52, 0x6f, 0x6f, - 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x68, 0x61, - 0x72, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x45, 0x0a, 0x09, 0x70, 0x61, - 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, - 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, - 0x68, 0x74, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, - 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x39, 0x0a, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x18, 0x0f, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, - 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x2e, 0x76, 0x30, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x6f, 0x52, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, - 0x12, 0x48, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x10, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, - 0x30, 0x2e, 0x47, 0x65, 0x6f, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, - 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4e, 0x0a, 0x0e, 0x72, 0x65, - 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, - 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x0c, 0x72, 0x65, - 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x05, 0x69, 0x6d, - 0x61, 0x67, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65, 0x6e, - 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x05, - 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x39, 0x0a, 0x05, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x18, 0x13, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, - 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x2e, 0x76, 0x30, 0x2e, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x52, 0x05, 0x70, 0x68, 0x6f, 0x74, 0x6f, - 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x14, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x09, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x22, 0x5b, - 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x3c, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, + 0x67, 0x68, 0x74, 0x22, 0x56, 0x0a, 0x07, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x12, 0x19, + 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, + 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x68, 0x65, 0x69, + 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x48, 0x01, 0x52, 0x06, 0x68, 0x65, 0x69, + 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x77, 0x69, 0x64, 0x74, 0x68, + 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x9d, 0x01, 0x0a, 0x0e, + 0x47, 0x65, 0x6f, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x12, 0x1f, + 0x0a, 0x08, 0x61, 0x6c, 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, + 0x48, 0x00, 0x52, 0x08, 0x61, 0x6c, 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x88, 0x01, 0x01, 0x12, + 0x1f, 0x0a, 0x08, 0x6c, 0x61, 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x01, 0x48, 0x01, 0x52, 0x08, 0x6c, 0x61, 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x88, 0x01, 0x01, + 0x12, 0x21, 0x0a, 0x09, 0x6c, 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x01, 0x48, 0x02, 0x52, 0x09, 0x6c, 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, + 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x61, 0x6c, 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, + 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6c, 0x61, 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x42, 0x0c, 0x0a, + 0x0a, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, 0x22, 0x9b, 0x04, 0x0a, 0x05, + 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x12, 0x23, 0x0a, 0x0a, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x4d, + 0x61, 0x6b, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x63, 0x61, 0x6d, + 0x65, 0x72, 0x61, 0x4d, 0x61, 0x6b, 0x65, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0b, 0x63, 0x61, + 0x6d, 0x65, 0x72, 0x61, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, + 0x01, 0x52, 0x0b, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x88, 0x01, + 0x01, 0x12, 0x35, 0x0a, 0x13, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x75, 0x72, 0x65, 0x44, 0x65, 0x6e, + 0x6f, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x48, 0x02, + 0x52, 0x13, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x75, 0x72, 0x65, 0x44, 0x65, 0x6e, 0x6f, 0x6d, 0x69, + 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x12, 0x31, 0x0a, 0x11, 0x65, 0x78, 0x70, 0x6f, + 0x73, 0x75, 0x72, 0x65, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x02, 0x48, 0x03, 0x52, 0x11, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x75, 0x72, 0x65, 0x4e, + 0x75, 0x6d, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x66, + 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x02, 0x48, 0x04, 0x52, 0x07, + 0x66, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0b, 0x66, 0x6f, + 0x63, 0x61, 0x6c, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x02, 0x48, + 0x05, 0x52, 0x0b, 0x66, 0x6f, 0x63, 0x61, 0x6c, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x88, 0x01, + 0x01, 0x12, 0x15, 0x0a, 0x03, 0x69, 0x73, 0x6f, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x06, + 0x52, 0x03, 0x69, 0x73, 0x6f, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0b, 0x6f, 0x72, 0x69, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x48, 0x07, 0x52, + 0x0b, 0x6f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, + 0x45, 0x0a, 0x0d, 0x74, 0x61, 0x6b, 0x65, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x48, 0x08, 0x52, 0x0d, 0x74, 0x61, 0x6b, 0x65, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x63, 0x61, 0x6d, 0x65, 0x72, + 0x61, 0x4d, 0x61, 0x6b, 0x65, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, + 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x75, + 0x72, 0x65, 0x44, 0x65, 0x6e, 0x6f, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x42, 0x14, 0x0a, + 0x12, 0x5f, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x75, 0x72, 0x65, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x61, + 0x74, 0x6f, 0x72, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x66, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x42, + 0x0e, 0x0a, 0x0c, 0x5f, 0x66, 0x6f, 0x63, 0x61, 0x6c, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, + 0x06, 0x0a, 0x04, 0x5f, 0x69, 0x73, 0x6f, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x6f, 0x72, 0x69, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x74, 0x61, 0x6b, 0x65, + 0x6e, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xbb, 0x07, 0x0a, 0x06, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, + 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x03, 0x72, 0x65, 0x66, 0x12, + 0x38, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, + 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, + 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x65, 0x74, 0x61, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x65, 0x74, 0x61, + 0x67, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x48, 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, + 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0d, + 0x73, 0x68, 0x61, 0x72, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x45, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, + 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, + 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, + 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1e, 0x0a, + 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x39, 0x0a, + 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, + 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x41, 0x75, 0x64, 0x69, + 0x6f, 0x52, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x12, 0x48, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x70, 0x65, + 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x47, 0x65, 0x6f, 0x43, 0x6f, 0x6f, + 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x4e, 0x0a, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x69, 0x74, 0x65, + 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, + 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x44, 0x52, 0x0c, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, + 0x49, 0x64, 0x12, 0x39, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, + 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x39, 0x0a, + 0x05, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, + 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x50, 0x68, 0x6f, 0x74, + 0x6f, 0x52, 0x05, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x61, 0x76, 0x6f, + 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x66, 0x61, 0x76, + 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x3f, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, + 0x77, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x4d, 0x5a, 0x4b, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, - 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x70, - 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, - 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x07, + 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x22, 0x5b, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, + 0x12, 0x3c, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x24, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, + 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, + 0x63, 0x6f, 0x72, 0x65, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, + 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, + 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -990,36 +1064,38 @@ func file_opencloud_messages_search_v0_search_proto_rawDescGZIP() []byte { return file_opencloud_messages_search_v0_search_proto_rawDescData } -var file_opencloud_messages_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_opencloud_messages_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_opencloud_messages_search_v0_search_proto_goTypes = []interface{}{ (*ResourceID)(nil), // 0: opencloud.messages.search.v0.ResourceID (*Reference)(nil), // 1: opencloud.messages.search.v0.Reference (*Audio)(nil), // 2: opencloud.messages.search.v0.Audio (*Image)(nil), // 3: opencloud.messages.search.v0.Image - (*GeoCoordinates)(nil), // 4: opencloud.messages.search.v0.GeoCoordinates - (*Photo)(nil), // 5: opencloud.messages.search.v0.Photo - (*Entity)(nil), // 6: opencloud.messages.search.v0.Entity - (*Match)(nil), // 7: opencloud.messages.search.v0.Match - (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*Preview)(nil), // 4: opencloud.messages.search.v0.Preview + (*GeoCoordinates)(nil), // 5: opencloud.messages.search.v0.GeoCoordinates + (*Photo)(nil), // 6: opencloud.messages.search.v0.Photo + (*Entity)(nil), // 7: opencloud.messages.search.v0.Entity + (*Match)(nil), // 8: opencloud.messages.search.v0.Match + (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp } var file_opencloud_messages_search_v0_search_proto_depIdxs = []int32{ 0, // 0: opencloud.messages.search.v0.Reference.resource_id:type_name -> opencloud.messages.search.v0.ResourceID - 8, // 1: opencloud.messages.search.v0.Photo.takenDateTime:type_name -> google.protobuf.Timestamp + 9, // 1: opencloud.messages.search.v0.Photo.takenDateTime:type_name -> google.protobuf.Timestamp 1, // 2: opencloud.messages.search.v0.Entity.ref:type_name -> opencloud.messages.search.v0.Reference 0, // 3: opencloud.messages.search.v0.Entity.id:type_name -> opencloud.messages.search.v0.ResourceID - 8, // 4: opencloud.messages.search.v0.Entity.last_modified_time:type_name -> google.protobuf.Timestamp + 9, // 4: opencloud.messages.search.v0.Entity.last_modified_time:type_name -> google.protobuf.Timestamp 0, // 5: opencloud.messages.search.v0.Entity.parent_id:type_name -> opencloud.messages.search.v0.ResourceID 2, // 6: opencloud.messages.search.v0.Entity.audio:type_name -> opencloud.messages.search.v0.Audio - 4, // 7: opencloud.messages.search.v0.Entity.location:type_name -> opencloud.messages.search.v0.GeoCoordinates + 5, // 7: opencloud.messages.search.v0.Entity.location:type_name -> opencloud.messages.search.v0.GeoCoordinates 0, // 8: opencloud.messages.search.v0.Entity.remote_item_id:type_name -> opencloud.messages.search.v0.ResourceID 3, // 9: opencloud.messages.search.v0.Entity.image:type_name -> opencloud.messages.search.v0.Image - 5, // 10: opencloud.messages.search.v0.Entity.photo:type_name -> opencloud.messages.search.v0.Photo - 6, // 11: opencloud.messages.search.v0.Match.entity:type_name -> opencloud.messages.search.v0.Entity - 12, // [12:12] is the sub-list for method output_type - 12, // [12:12] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 6, // 10: opencloud.messages.search.v0.Entity.photo:type_name -> opencloud.messages.search.v0.Photo + 4, // 11: opencloud.messages.search.v0.Entity.preview:type_name -> opencloud.messages.search.v0.Preview + 7, // 12: opencloud.messages.search.v0.Match.entity:type_name -> opencloud.messages.search.v0.Entity + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_opencloud_messages_search_v0_search_proto_init() } @@ -1077,7 +1153,7 @@ func file_opencloud_messages_search_v0_search_proto_init() { } } file_opencloud_messages_search_v0_search_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GeoCoordinates); i { + switch v := v.(*Preview); i { case 0: return &v.state case 1: @@ -1089,7 +1165,7 @@ func file_opencloud_messages_search_v0_search_proto_init() { } } file_opencloud_messages_search_v0_search_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Photo); i { + switch v := v.(*GeoCoordinates); i { case 0: return &v.state case 1: @@ -1101,7 +1177,7 @@ func file_opencloud_messages_search_v0_search_proto_init() { } } file_opencloud_messages_search_v0_search_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Entity); i { + switch v := v.(*Photo); i { case 0: return &v.state case 1: @@ -1113,6 +1189,18 @@ func file_opencloud_messages_search_v0_search_proto_init() { } } file_opencloud_messages_search_v0_search_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Entity); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_opencloud_messages_search_v0_search_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Match); i { case 0: return &v.state @@ -1129,13 +1217,14 @@ func file_opencloud_messages_search_v0_search_proto_init() { file_opencloud_messages_search_v0_search_proto_msgTypes[3].OneofWrappers = []interface{}{} file_opencloud_messages_search_v0_search_proto_msgTypes[4].OneofWrappers = []interface{}{} file_opencloud_messages_search_v0_search_proto_msgTypes[5].OneofWrappers = []interface{}{} + file_opencloud_messages_search_v0_search_proto_msgTypes[6].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_opencloud_messages_search_v0_search_proto_rawDesc, NumEnums: 0, - NumMessages: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 0, }, diff --git a/protogen/gen/opencloud/messages/search/v0/search.pb.web.go b/protogen/gen/opencloud/messages/search/v0/search.pb.web.go index 22c3e08238..a1720ffefb 100644 --- a/protogen/gen/opencloud/messages/search/v0/search.pb.web.go +++ b/protogen/gen/opencloud/messages/search/v0/search.pb.web.go @@ -154,6 +154,42 @@ func (m *Image) UnmarshalJSON(b []byte) error { var _ json.Unmarshaler = (*Image)(nil) +// PreviewJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of Preview. This struct is safe to replace or modify but +// should not be done so concurrently. +var PreviewJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *Preview) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := PreviewJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*Preview)(nil) + +// PreviewJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of Preview. This struct is safe to replace or modify but +// should not be done so concurrently. +var PreviewJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *Preview) UnmarshalJSON(b []byte) error { + return PreviewJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*Preview)(nil) + // GeoCoordinatesJSONMarshaler describes the default jsonpb.Marshaler used by all // instances of GeoCoordinates. This struct is safe to replace or modify but // should not be done so concurrently. diff --git a/protogen/gen/opencloud/services/search/v0/search.swagger.json b/protogen/gen/opencloud/services/search/v0/search.swagger.json index a8d7b9bd8f..5b020aefb6 100644 --- a/protogen/gen/opencloud/services/search/v0/search.swagger.json +++ b/protogen/gen/opencloud/services/search/v0/search.swagger.json @@ -288,6 +288,9 @@ "items": { "type": "string" } + }, + "preview": { + "$ref": "#/definitions/v0Preview" } } }, @@ -391,6 +394,20 @@ } } }, + "v0Preview": { + "type": "object", + "properties": { + "width": { + "type": "integer", + "format": "int32" + }, + "height": { + "type": "integer", + "format": "int32" + } + }, + "description": "Preview carries the dimensions of an embedded preview (e.g. audio cover art).\nIts presence signals that a preview is available for the resource." + }, "v0Reference": { "type": "object", "properties": { diff --git a/protogen/proto/opencloud/messages/search/v0/search.proto b/protogen/proto/opencloud/messages/search/v0/search.proto index 4633a2cee7..57a57fffef 100644 --- a/protogen/proto/opencloud/messages/search/v0/search.proto +++ b/protogen/proto/opencloud/messages/search/v0/search.proto @@ -41,6 +41,13 @@ message Image { optional int32 height = 2; } +// Preview carries the dimensions of an embedded preview (e.g. audio cover art). +// Its presence signals that a preview is available for the resource. +message Preview { + optional int32 width = 1; + optional int32 height = 2; +} + message GeoCoordinates { optional double altitude = 1; optional double latitude = 2; @@ -80,6 +87,7 @@ message Entity { Image image = 18; Photo photo = 19; repeated string favorites = 20; + Preview preview = 21; } message Match { diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 1acd7b9215..6f4f6172c7 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -209,6 +209,7 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } + g.setDriveItemsThumbnails(r, files, lRes.GetInfos()) render.Status(r, http.StatusOK) render.JSON(w, r, &ListResponse{Value: files}) @@ -341,6 +342,7 @@ func (g Graph) GetDriveItemChildren(w http.ResponseWriter, r *http.Request) { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } + g.setDriveItemsThumbnails(r, files, res.GetInfos()) render.Status(r, http.StatusOK) render.JSON(w, r, &ListResponse{Value: files}) diff --git a/services/graph/pkg/service/v0/thumbnails.go b/services/graph/pkg/service/v0/thumbnails.go new file mode 100644 index 0000000000..8acd1e9374 --- /dev/null +++ b/services/graph/pkg/service/v0/thumbnails.go @@ -0,0 +1,81 @@ +package svc + +import ( + "fmt" + "net/http" + "strings" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + "github.com/opencloud-eu/reva/v2/pkg/storagespace" + + "github.com/opencloud-eu/opencloud/pkg/conversions" + "github.com/opencloud-eu/opencloud/services/thumbnails/pkg/thumbnail" +) + +func shouldExpand(r *http.Request, relationship string) bool { + return strings.Contains(r.URL.Query().Get("$expand"), relationship) +} + +func (g Graph) setDriveItemsThumbnails(r *http.Request, items []*libregraph.DriveItem, infos []*provider.ResourceInfo) { + if !shouldExpand(r, "thumbnails") { + return + } + for i := range items { + if i < len(infos) { + setDriveItemThumbnails(items[i], infos[i], g.config.Commons.OpenCloudURL) + } + } +} + +// Requested box sizes for the thumbnail URLs. Small/medium/large carry no +// dimensions: the endpoint rounds the box to a configured resolution and fits +// within it, so the served size is not known here. Only the source dimensions +// (below) are exact. +const ( + thumbnailBoxSmall = 36 + thumbnailBoxMedium = 48 + thumbnailBoxLarge = 96 +) + +func setDriveItemThumbnails(item *libregraph.DriveItem, res *provider.ResourceInfo, baseURL string) { + if set := previewThumbnailSet(res, baseURL); set != nil { + item.SetThumbnails([]libregraph.ThumbnailSet{*set}) + } +} + +func previewThumbnailSet(res *provider.ResourceInfo, baseURL string) *libregraph.ThumbnailSet { + if !thumbnail.HasPreview(res) { + return nil + } + + base := fmt.Sprintf("%s/dav/spaces/%s?scalingup=0&preview=1&processor=thumbnail", + baseURL, storagespace.FormatResourceID(res.GetId())) + + set := &libregraph.ThumbnailSet{ + Small: previewThumbnail(base, thumbnailBoxSmall), + Medium: previewThumbnail(base, thumbnailBoxMedium), + Large: previewThumbnail(base, thumbnailBoxLarge), + } + if w, h := previewSourceDimensions(res); w > 0 && h > 0 { + url := fmt.Sprintf("%s&x=%d&y=%d", base, w, h) + set.Source = &libregraph.Thumbnail{Url: &url, Width: &w, Height: &h} + } + return set +} + +func previewThumbnail(base string, box int32) *libregraph.Thumbnail { + url := fmt.Sprintf("%s&x=%d&y=%d", base, box, box) + return &libregraph.Thumbnail{Url: &url} +} + +// previewSourceDimensions: audio cover from oc.preview, images from the image facet. +func previewSourceDimensions(res *provider.ResourceInfo) (int32, int32) { + if w, h := thumbnail.PreviewDimensions(res); w > 0 && h > 0 { + return w, h + } + meta := res.GetArbitraryMetadata().GetMetadata() + w := conversions.StringToInt32(meta["libre.graph.image.width"], 0) + h := conversions.StringToInt32(meta["libre.graph.image.height"], 0) + return w, h +} diff --git a/services/search/pkg/bleve/backend.go b/services/search/pkg/bleve/backend.go index b4088918fe..cf74bc8a03 100644 --- a/services/search/pkg/bleve/backend.go +++ b/services/search/pkg/bleve/backend.go @@ -141,6 +141,7 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques Image: hitToFacet[searchMessage.Image](hit.Fields, "image"), Location: hitToFacet[searchMessage.GeoCoordinates](hit.Fields, "location"), Photo: hitToFacet[searchMessage.Photo](hit.Fields, "photo"), + Preview: hitToFacet[searchMessage.Preview](hit.Fields, "preview"), }, } diff --git a/services/search/pkg/content/content.go b/services/search/pkg/content/content.go index a789847244..87b9e2501d 100644 --- a/services/search/pkg/content/content.go +++ b/services/search/pkg/content/content.go @@ -27,6 +27,22 @@ type Document struct { Image *libregraph.Image `json:"image,omitempty"` Location *libregraph.GeoCoordinates `json:"location,omitempty"` Photo *libregraph.Photo `json:"photo,omitempty"` + Preview *Preview `json:"preview,omitempty"` +} + +// Preview holds the dimensions of an embedded preview (e.g. audio cover art). +// Internal signal, not a Graph facet; its presence marks that a preview exists. +type Preview struct { + Width int32 `json:"width"` + Height int32 `json:"height"` +} + +// ToMap flows Preview through the shared facet flattening, under oc.preview. +func (p Preview) ToMap() (map[string]interface{}, error) { + return map[string]interface{}{ + "width": p.Width, + "height": p.Height, + }, nil } func CleanString(content, langCode string) string { diff --git a/services/search/pkg/content/tika.go b/services/search/pkg/content/tika.go index 25e733ff65..c086ab69d2 100644 --- a/services/search/pkg/content/tika.go +++ b/services/search/pkg/content/tika.go @@ -77,7 +77,11 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, if err != nil { return doc, err } + if len(metas) == 0 { + return doc, nil + } + // Title and content aggregate across the container and embedded resources. for _, meta := range metas { if title, err := getFirstValue(meta, "title"); err == nil { doc.Title = strings.TrimSpace(fmt.Sprintf("%s %s", doc.Title, title)) @@ -86,13 +90,18 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, if content, err := getFirstValue(meta, "X-TIKA:content"); err == nil { doc.Content = strings.TrimSpace(fmt.Sprintf("%s %s", doc.Content, content)) } - - doc.Location = t.getLocation(meta) - doc.Image = t.getImage(meta) - doc.Photo = t.getPhoto(meta) - doc.Audio = t.getAudio(meta) } + // Facets come from the container (first entry) only; embedded resources like + // audio cover art must not leak in (the cover becomes the preview instead). + container := metas[0] + doc.Location = t.getLocation(container) + doc.Image = t.getImage(container) + doc.Photo = t.getPhoto(container) + doc.Audio = t.getAudio(container) + + doc.Preview = getPreview(ri.GetMimeType(), metas) + if langCode, _ := t.tika.LanguageString(ctx, doc.Content); langCode != "" && t.CleanStopWords { doc.Content = CleanString(doc.Content, langCode) } diff --git a/services/search/pkg/content/tika_preview.go b/services/search/pkg/content/tika_preview.go new file mode 100644 index 0000000000..76a238e562 --- /dev/null +++ b/services/search/pkg/content/tika_preview.go @@ -0,0 +1,47 @@ +package content + +import ( + "strconv" + "strings" + + "github.com/opencloud-eu/opencloud/services/thumbnails/pkg/thumbnail" +) + +// frontCoverDescription is the picture type Tika reports (as dc:description) for +// the front cover, matching the thumbnailer's cover selection. +const frontCoverDescription = "Cover (front)" + +// getPreview returns the dimensions of an audio file's embedded cover art from +// Tika's recursive metadata, preferring the front cover (dc:description) and +// falling back to the first image, matching the thumbnailer's selection. It only +// runs for EmbeddedPreviewMimeTypes. +func getPreview(mimeType string, metas []map[string][]string) *Preview { + if _, ok := thumbnail.EmbeddedPreviewMimeTypes[mimeType]; !ok { + return nil + } + var first *Preview + for _, meta := range metas { + ct, err := getFirstValue(meta, "Content-Type") + if err != nil || !strings.HasPrefix(ct, "image/") { + continue + } + w, wErr := getFirstValue(meta, "tiff:ImageWidth") + h, hErr := getFirstValue(meta, "tiff:ImageLength") + if wErr != nil || hErr != nil { + continue + } + width, wErr := strconv.ParseInt(w, 10, 32) + height, hErr := strconv.ParseInt(h, 10, 32) + if wErr != nil || hErr != nil || width <= 0 || height <= 0 { + continue + } + preview := &Preview{Width: int32(width), Height: int32(height)} + if desc, _ := getFirstValue(meta, "dc:description"); desc == frontCoverDescription { + return preview + } + if first == nil { + first = preview + } + } + return first +} diff --git a/services/search/pkg/content/tika_preview_test.go b/services/search/pkg/content/tika_preview_test.go new file mode 100644 index 0000000000..f57055d768 --- /dev/null +++ b/services/search/pkg/content/tika_preview_test.go @@ -0,0 +1,49 @@ +package content + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("getPreview", func() { + audio := map[string][]string{"Content-Type": {"audio/mpeg"}} + cover := map[string][]string{ + "Content-Type": {"image/jpeg"}, + "tiff:ImageWidth": {"500"}, + "tiff:ImageLength": {"400"}, + } + coverNoDims := map[string][]string{"Content-Type": {"image/jpeg"}} + + It("returns the embedded cover dimensions for audio", func() { + p := getPreview("audio/mpeg", []map[string][]string{audio, cover}) + Expect(p).ToNot(BeNil()) + Expect(*p).To(Equal(Preview{Width: 500, Height: 400})) + }) + + It("returns nil when the audio has no cover", func() { + Expect(getPreview("audio/mpeg", []map[string][]string{audio})).To(BeNil()) + }) + + It("returns nil when the cover lacks dimensions", func() { + Expect(getPreview("audio/mpeg", []map[string][]string{audio, coverNoDims})).To(BeNil()) + }) + + It("prefers the front cover over an earlier back cover", func() { + back := map[string][]string{ + "Content-Type": {"image/jpeg"}, "dc:description": {"Cover (back)"}, + "tiff:ImageWidth": {"30"}, "tiff:ImageLength": {"30"}, + } + front := map[string][]string{ + "Content-Type": {"image/jpeg"}, "dc:description": {"Cover (front)"}, + "tiff:ImageWidth": {"64"}, "tiff:ImageLength": {"40"}, + } + p := getPreview("audio/mpeg", []map[string][]string{audio, back, front}) + Expect(p).ToNot(BeNil()) + Expect(*p).To(Equal(Preview{Width: 64, Height: 40})) + }) + + It("is gated to embedded-preview types", func() { + // an image is unconditional; its preview is not driven by oc.preview. + Expect(getPreview("image/png", []map[string][]string{cover})).To(BeNil()) + }) +}) diff --git a/services/search/pkg/opensearch/internal/convert/opensearch.go b/services/search/pkg/opensearch/internal/convert/opensearch.go index 01c662096c..864ea9f24e 100644 --- a/services/search/pkg/opensearch/internal/convert/opensearch.go +++ b/services/search/pkg/opensearch/internal/convert/opensearch.go @@ -82,6 +82,7 @@ func OpenSearchHitToMatch(hit opensearchgoAPI.SearchHit) (*searchMessage.Match, Image: copyFacet[searchMessage.Image](resource.Image), Location: copyFacet[searchMessage.GeoCoordinates](resource.Location), Photo: copyFacet[searchMessage.Photo](resource.Photo), + Preview: copyFacet[searchMessage.Preview](resource.Preview), }, } diff --git a/services/search/pkg/search/service.go b/services/search/pkg/search/service.go index 2c8221b2a7..1b190710b7 100644 --- a/services/search/pkg/search/service.go +++ b/services/search/pkg/search/service.go @@ -633,6 +633,7 @@ func (s *Service) doUpsertItem(ref *provider.Reference, batch BatchOperator) { facetToMetadata(metadata, doc.Image, "libre.graph.image.") facetToMetadata(metadata, doc.Location, "libre.graph.location.") facetToMetadata(metadata, doc.Photo, "libre.graph.photo.") + facetToMetadata(metadata, doc.Preview, "oc.preview.") if len(metadata) == 0 { return } diff --git a/services/thumbnails/pkg/thumbnail/has_preview.go b/services/thumbnails/pkg/thumbnail/has_preview.go new file mode 100644 index 0000000000..6bd418a5b7 --- /dev/null +++ b/services/thumbnails/pkg/thumbnail/has_preview.go @@ -0,0 +1,38 @@ +package thumbnail + +import ( + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + + "github.com/opencloud-eu/opencloud/pkg/conversions" +) + +// Arbitrary-metadata keys for an embedded preview's dimensions, written at index +// time; their presence signals a preview exists. +const ( + PreviewWidthKey = "oc.preview.width" + PreviewHeightKey = "oc.preview.height" +) + +// HasPreview reports whether a preview can be produced for the resource. +func HasPreview(md *provider.ResourceInfo) bool { + w, h := PreviewDimensions(md) + return HasPreviewForMimeType(md.GetMimeType(), w > 0 && h > 0) +} + +// HasPreviewForMimeType is HasPreview for callers with only the mimetype and a +// presence signal (e.g. search results). +func HasPreviewForMimeType(mimeType string, hasEmbeddedPreview bool) bool { + if _, ok := UnconditionalPreviewMimeTypes[mimeType]; ok { + return true + } + _, embedded := EmbeddedPreviewMimeTypes[mimeType] + return embedded && hasEmbeddedPreview +} + +// PreviewDimensions returns the stored embedded-preview dimensions, or (0, 0). +func PreviewDimensions(md *provider.ResourceInfo) (int32, int32) { + meta := md.GetArbitraryMetadata().GetMetadata() + w := conversions.StringToInt32(meta[PreviewWidthKey], 0) + h := conversions.StringToInt32(meta[PreviewHeightKey], 0) + return w, h +} diff --git a/services/thumbnails/pkg/thumbnail/has_preview_test.go b/services/thumbnails/pkg/thumbnail/has_preview_test.go new file mode 100644 index 0000000000..271e091cca --- /dev/null +++ b/services/thumbnails/pkg/thumbnail/has_preview_test.go @@ -0,0 +1,45 @@ +package thumbnail_test + +import ( + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/opencloud-eu/opencloud/services/thumbnails/pkg/thumbnail" +) + +func resourceInfo(mime string, meta map[string]string) *provider.ResourceInfo { + ri := &provider.ResourceInfo{MimeType: mime} + if meta != nil { + ri.ArbitraryMetadata = &provider.ArbitraryMetadata{Metadata: meta} + } + return ri +} + +var _ = Describe("HasPreview", func() { + DescribeTable("preview availability", + func(md *provider.ResourceInfo, want bool) { + Expect(thumbnail.HasPreview(md)).To(Equal(want)) + }, + Entry("nil", nil, false), + Entry("unconditional image", resourceInfo("image/png", nil), true), + Entry("unconditional text", resourceInfo("text/plain", nil), true), + Entry("unsupported type", resourceInfo("application/pdf", nil), false), + Entry("audio without preview dims", resourceInfo("audio/mpeg", nil), false), + Entry("audio with zero dims", resourceInfo("audio/mpeg", map[string]string{ + thumbnail.PreviewWidthKey: "0", thumbnail.PreviewHeightKey: "0", + }), false), + Entry("audio with preview dims", resourceInfo("audio/mpeg", map[string]string{ + thumbnail.PreviewWidthKey: "500", thumbnail.PreviewHeightKey: "500", + }), true), + ) + + It("SupportedMimeTypes is the union of both preview sets", func() { + for k := range thumbnail.UnconditionalPreviewMimeTypes { + Expect(thumbnail.SupportedMimeTypes).To(HaveKey(k)) + } + for k := range thumbnail.EmbeddedPreviewMimeTypes { + Expect(thumbnail.SupportedMimeTypes).To(HaveKey(k)) + } + }) +}) diff --git a/services/thumbnails/pkg/thumbnail/mimetypes.go b/services/thumbnails/pkg/thumbnail/mimetypes.go index 02a91e22f5..390cec01c7 100644 --- a/services/thumbnails/pkg/thumbnail/mimetypes.go +++ b/services/thumbnails/pkg/thumbnail/mimetypes.go @@ -2,21 +2,17 @@ package thumbnail -var ( - // SupportedMimeTypes contains an all mimetypes which are supported by the thumbnailer. - SupportedMimeTypes = map[string]struct{}{ - "image/png": {}, - "image/jpg": {}, - "image/jpeg": {}, - "image/gif": {}, - "image/bmp": {}, - "image/x-ms-bmp": {}, - "image/tiff": {}, - "text/plain": {}, - "audio/flac": {}, - "audio/mpeg": {}, - "audio/ogg": {}, - "application/vnd.geogebra.slides": {}, - "application/vnd.geogebra.pinboard": {}, - } -) +// UnconditionalPreviewMimeTypes always have a preview: the thumbnailer renders +// one from the content. +var UnconditionalPreviewMimeTypes = map[string]struct{}{ + "image/png": {}, + "image/jpg": {}, + "image/jpeg": {}, + "image/gif": {}, + "image/bmp": {}, + "image/x-ms-bmp": {}, + "image/tiff": {}, + "text/plain": {}, + "application/vnd.geogebra.slides": {}, + "application/vnd.geogebra.pinboard": {}, +} diff --git a/services/thumbnails/pkg/thumbnail/mimetypes_common.go b/services/thumbnails/pkg/thumbnail/mimetypes_common.go new file mode 100644 index 0000000000..96f047c713 --- /dev/null +++ b/services/thumbnails/pkg/thumbnail/mimetypes_common.go @@ -0,0 +1,21 @@ +package thumbnail + +// EmbeddedPreviewMimeTypes have an embedded preview (e.g. audio cover art) that +// may be absent; availability is decided per file (see HasPreview). +var EmbeddedPreviewMimeTypes = map[string]struct{}{ + "audio/flac": {}, + "audio/mpeg": {}, + "audio/ogg": {}, +} + +// SupportedMimeTypes is the union of both preview sets; the generator gates on it. +var SupportedMimeTypes = func() map[string]struct{} { + m := make(map[string]struct{}, len(UnconditionalPreviewMimeTypes)+len(EmbeddedPreviewMimeTypes)) + for k := range UnconditionalPreviewMimeTypes { + m[k] = struct{}{} + } + for k := range EmbeddedPreviewMimeTypes { + m[k] = struct{}{} + } + return m +}() diff --git a/services/thumbnails/pkg/thumbnail/mimetypes_vips.go b/services/thumbnails/pkg/thumbnail/mimetypes_vips.go index b94fafafaf..929718e7ad 100644 --- a/services/thumbnails/pkg/thumbnail/mimetypes_vips.go +++ b/services/thumbnails/pkg/thumbnail/mimetypes_vips.go @@ -2,22 +2,18 @@ package thumbnail -var ( - // SupportedMimeTypes contains an all mimetypes which are supported by the thumbnailer. - SupportedMimeTypes = map[string]struct{}{ - "image/png": {}, - "image/jpg": {}, - "image/jpeg": {}, - "image/gif": {}, - "image/bmp": {}, - "image/x-ms-bmp": {}, - "image/tiff": {}, - "text/plain": {}, - "audio/flac": {}, - "audio/mpeg": {}, - "audio/ogg": {}, - "application/vnd.geogebra.slides": {}, - "application/vnd.geogebra.pinboard": {}, - "image/webp": {}, - } -) +// UnconditionalPreviewMimeTypes always have a preview: the thumbnailer renders +// one from the content. +var UnconditionalPreviewMimeTypes = map[string]struct{}{ + "image/png": {}, + "image/jpg": {}, + "image/jpeg": {}, + "image/gif": {}, + "image/bmp": {}, + "image/x-ms-bmp": {}, + "image/tiff": {}, + "image/webp": {}, + "text/plain": {}, + "application/vnd.geogebra.slides": {}, + "application/vnd.geogebra.pinboard": {}, +} diff --git a/services/thumbnails/pkg/thumbnail/thumbnail_suite_test.go b/services/thumbnails/pkg/thumbnail/thumbnail_suite_test.go new file mode 100644 index 0000000000..7217b6a146 --- /dev/null +++ b/services/thumbnails/pkg/thumbnail/thumbnail_suite_test.go @@ -0,0 +1,13 @@ +package thumbnail_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestThumbnail(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Thumbnail Suite") +} diff --git a/services/webdav/pkg/service/v0/search.go b/services/webdav/pkg/service/v0/search.go index e2f6a3f4a4..8da71806d9 100644 --- a/services/webdav/pkg/service/v0/search.go +++ b/services/webdav/pkg/service/v0/search.go @@ -222,8 +222,7 @@ func matchToPropResponse(ctx context.Context, davPrefix, publicURL string, match propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:permissions", match.Entity.Permissions)) propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:highlights", match.Entity.Highlights)) propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getcontenttype", match.Entity.MimeType)) - _, isSupportedMimeType := thumbnail.SupportedMimeTypes[match.Entity.MimeType] - if isSupportedMimeType { + if thumbnail.HasPreviewForMimeType(match.Entity.MimeType, match.Entity.GetPreview() != nil) { propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:has-preview", "1")) } else { propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:has-preview", "0"))