From db32fb46f98485140e4a9aa6b3799e7a45d5c2e8 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 6 Nov 2023 08:56:46 +0100 Subject: [PATCH] Audio Metadata (#7490) * Add audio facet to search protobuf message * Add audio metadata to search index * Return audio facet from search if available * Store audio metadata in arbitrary metadata * Add audio facet to driveItems listings * Make tests coding style more consistent * Fix tests * Add changelog * Make valueToString code more defensive * Log status code as well --- ...ancement-store-and-index-audio-metadata.md | 6 + .../gen/ocis/messages/search/v0/search.pb.go | 364 +++++++++++++++--- .../ocis/messages/search/v0/search.pb.web.go | 36 ++ .../ocis/messages/settings/v0/settings.pb.go | 98 ++--- .../services/search/v0/search.swagger.json | 63 +++ .../ocis/messages/search/v0/search.proto | 20 + services/graph/pkg/service/v0/driveitems.go | 88 ++++- .../graph/pkg/service/v0/driveitems_test.go | 131 +++++-- services/search/pkg/content/content.go | 2 + services/search/pkg/content/tika.go | 64 +++ services/search/pkg/content/tika_test.go | 69 +++- services/search/pkg/engine/bleve.go | 53 +++ services/search/pkg/engine/bleve_test.go | 66 ++++ services/search/pkg/search/service.go | 67 ++++ 14 files changed, 974 insertions(+), 153 deletions(-) create mode 100644 changelog/unreleased/enhancement-store-and-index-audio-metadata.md diff --git a/changelog/unreleased/enhancement-store-and-index-audio-metadata.md b/changelog/unreleased/enhancement-store-and-index-audio-metadata.md new file mode 100644 index 000000000..372f3a570 --- /dev/null +++ b/changelog/unreleased/enhancement-store-and-index-audio-metadata.md @@ -0,0 +1,6 @@ +Enhancement: Store and index metadata + +Audio metadata is now extracted and stored by the search service. +It is available for driveItems in a folder listing using the Graph API. + +https://github.com/owncloud/ocis/pull/7490 diff --git a/protogen/gen/ocis/messages/search/v0/search.pb.go b/protogen/gen/ocis/messages/search/v0/search.pb.go index e9eabf8a7..267fcf0e6 100644 --- a/protogen/gen/ocis/messages/search/v0/search.pb.go +++ b/protogen/gen/ocis/messages/search/v0/search.pb.go @@ -139,6 +139,173 @@ func (x *Reference) GetPath() string { return "" } +type Audio struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Album *string `protobuf:"bytes,1,opt,name=album,proto3,oneof" json:"album,omitempty"` + AlbumArtist *string `protobuf:"bytes,2,opt,name=albumArtist,proto3,oneof" json:"albumArtist,omitempty"` + Artist *string `protobuf:"bytes,3,opt,name=artist,proto3,oneof" json:"artist,omitempty"` + Bitrate *int64 `protobuf:"varint,4,opt,name=bitrate,proto3,oneof" json:"bitrate,omitempty"` + Composers *string `protobuf:"bytes,5,opt,name=composers,proto3,oneof" json:"composers,omitempty"` + Copyright *string `protobuf:"bytes,6,opt,name=copyright,proto3,oneof" json:"copyright,omitempty"` + Disc *int32 `protobuf:"varint,7,opt,name=disc,proto3,oneof" json:"disc,omitempty"` + DiscCount *int32 `protobuf:"varint,8,opt,name=discCount,proto3,oneof" json:"discCount,omitempty"` + Duration *int64 `protobuf:"varint,9,opt,name=duration,proto3,oneof" json:"duration,omitempty"` + Genre *string `protobuf:"bytes,10,opt,name=genre,proto3,oneof" json:"genre,omitempty"` + HasDrm *bool `protobuf:"varint,11,opt,name=hasDrm,proto3,oneof" json:"hasDrm,omitempty"` + IsVariableBitrate *bool `protobuf:"varint,12,opt,name=isVariableBitrate,proto3,oneof" json:"isVariableBitrate,omitempty"` + Title *string `protobuf:"bytes,13,opt,name=title,proto3,oneof" json:"title,omitempty"` + Track *int32 `protobuf:"varint,14,opt,name=track,proto3,oneof" json:"track,omitempty"` + TrackCount *int32 `protobuf:"varint,15,opt,name=trackCount,proto3,oneof" json:"trackCount,omitempty"` + Year *int32 `protobuf:"varint,16,opt,name=year,proto3,oneof" json:"year,omitempty"` +} + +func (x *Audio) Reset() { + *x = Audio{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Audio) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Audio) ProtoMessage() {} + +func (x *Audio) ProtoReflect() protoreflect.Message { + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[2] + 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 Audio.ProtoReflect.Descriptor instead. +func (*Audio) Descriptor() ([]byte, []int) { + return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{2} +} + +func (x *Audio) GetAlbum() string { + if x != nil && x.Album != nil { + return *x.Album + } + return "" +} + +func (x *Audio) GetAlbumArtist() string { + if x != nil && x.AlbumArtist != nil { + return *x.AlbumArtist + } + return "" +} + +func (x *Audio) GetArtist() string { + if x != nil && x.Artist != nil { + return *x.Artist + } + return "" +} + +func (x *Audio) GetBitrate() int64 { + if x != nil && x.Bitrate != nil { + return *x.Bitrate + } + return 0 +} + +func (x *Audio) GetComposers() string { + if x != nil && x.Composers != nil { + return *x.Composers + } + return "" +} + +func (x *Audio) GetCopyright() string { + if x != nil && x.Copyright != nil { + return *x.Copyright + } + return "" +} + +func (x *Audio) GetDisc() int32 { + if x != nil && x.Disc != nil { + return *x.Disc + } + return 0 +} + +func (x *Audio) GetDiscCount() int32 { + if x != nil && x.DiscCount != nil { + return *x.DiscCount + } + return 0 +} + +func (x *Audio) GetDuration() int64 { + if x != nil && x.Duration != nil { + return *x.Duration + } + return 0 +} + +func (x *Audio) GetGenre() string { + if x != nil && x.Genre != nil { + return *x.Genre + } + return "" +} + +func (x *Audio) GetHasDrm() bool { + if x != nil && x.HasDrm != nil { + return *x.HasDrm + } + return false +} + +func (x *Audio) GetIsVariableBitrate() bool { + if x != nil && x.IsVariableBitrate != nil { + return *x.IsVariableBitrate + } + return false +} + +func (x *Audio) GetTitle() string { + if x != nil && x.Title != nil { + return *x.Title + } + return "" +} + +func (x *Audio) GetTrack() int32 { + if x != nil && x.Track != nil { + return *x.Track + } + return 0 +} + +func (x *Audio) GetTrackCount() int32 { + if x != nil && x.TrackCount != nil { + return *x.TrackCount + } + return 0 +} + +func (x *Audio) GetYear() int32 { + if x != nil && x.Year != nil { + return *x.Year + } + return 0 +} + type Entity struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -158,12 +325,13 @@ type Entity struct { ParentId *ResourceID `protobuf:"bytes,12,opt,name=parent_id,json=parentId,proto3" json:"parent_id,omitempty"` Tags []string `protobuf:"bytes,13,rep,name=tags,proto3" json:"tags,omitempty"` Highlights string `protobuf:"bytes,14,opt,name=highlights,proto3" json:"highlights,omitempty"` + Audio *Audio `protobuf:"bytes,15,opt,name=audio,proto3" json:"audio,omitempty"` } func (x *Entity) Reset() { *x = Entity{} if protoimpl.UnsafeEnabled { - mi := &file_ocis_messages_search_v0_search_proto_msgTypes[2] + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -176,7 +344,7 @@ func (x *Entity) String() string { func (*Entity) ProtoMessage() {} func (x *Entity) ProtoReflect() protoreflect.Message { - mi := &file_ocis_messages_search_v0_search_proto_msgTypes[2] + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -189,7 +357,7 @@ func (x *Entity) ProtoReflect() protoreflect.Message { // Deprecated: Use Entity.ProtoReflect.Descriptor instead. func (*Entity) Descriptor() ([]byte, []int) { - return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{2} + return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{3} } func (x *Entity) GetRef() *Reference { @@ -290,6 +458,13 @@ func (x *Entity) GetHighlights() string { return "" } +func (x *Entity) GetAudio() *Audio { + if x != nil { + return x.Audio + } + return nil +} + type Match struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -304,7 +479,7 @@ type Match struct { func (x *Match) Reset() { *x = Match{} if protoimpl.UnsafeEnabled { - mi := &file_ocis_messages_search_v0_search_proto_msgTypes[3] + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -317,7 +492,7 @@ func (x *Match) String() string { func (*Match) ProtoMessage() {} func (x *Match) ProtoReflect() protoreflect.Message { - mi := &file_ocis_messages_search_v0_search_proto_msgTypes[3] + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -330,7 +505,7 @@ func (x *Match) ProtoReflect() protoreflect.Message { // Deprecated: Use Match.ProtoReflect.Descriptor instead. func (*Match) Descriptor() ([]byte, []int) { - return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{3} + return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{4} } func (x *Match) GetEntity() *Entity { @@ -368,50 +543,98 @@ var file_ocis_messages_search_v0_search_proto_rawDesc = []byte{ 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, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x82, 0x04, 0x0a, - 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x63, 0x69, 0x73, 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, 0x33, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x63, 0x69, 0x73, - 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, 0x40, 0x0a, 0x09, 0x70, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, - 0x2e, 0x6f, 0x63, 0x69, 0x73, 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, 0x22, 0x56, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x37, 0x0a, 0x06, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x63, 0x69, - 0x73, 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, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, - 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, - 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63, 0x69, 0x73, 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, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0xcf, 0x05, 0x0a, + 0x05, 0x41, 0x75, 0x64, 0x69, 0x6f, 0x12, 0x19, 0x0a, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x88, 0x01, + 0x01, 0x12, 0x25, 0x0a, 0x0b, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x41, 0x72, 0x74, 0x69, 0x73, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0b, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x41, + 0x72, 0x74, 0x69, 0x73, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x61, 0x72, 0x74, 0x69, + 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x06, 0x61, 0x72, 0x74, 0x69, + 0x73, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x62, 0x69, 0x74, 0x72, 0x61, 0x74, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x48, 0x03, 0x52, 0x07, 0x62, 0x69, 0x74, 0x72, 0x61, 0x74, + 0x65, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x73, 0x65, 0x72, + 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x04, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6f, + 0x73, 0x65, 0x72, 0x73, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x63, 0x6f, 0x70, 0x79, 0x72, + 0x69, 0x67, 0x68, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x05, 0x52, 0x09, 0x63, 0x6f, + 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, 0x04, 0x64, 0x69, + 0x73, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x06, 0x52, 0x04, 0x64, 0x69, 0x73, 0x63, + 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x63, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x48, 0x07, 0x52, 0x09, 0x64, 0x69, 0x73, 0x63, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x48, 0x08, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x67, 0x65, 0x6e, 0x72, 0x65, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x48, 0x09, 0x52, 0x05, 0x67, 0x65, 0x6e, 0x72, 0x65, 0x88, + 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x68, 0x61, 0x73, 0x44, 0x72, 0x6d, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x08, 0x48, 0x0a, 0x52, 0x06, 0x68, 0x61, 0x73, 0x44, 0x72, 0x6d, 0x88, 0x01, 0x01, 0x12, + 0x31, 0x0a, 0x11, 0x69, 0x73, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x69, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0b, 0x52, 0x11, 0x69, 0x73, + 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x69, 0x74, 0x72, 0x61, 0x74, 0x65, 0x88, + 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x0c, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, + 0x05, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x48, 0x0d, 0x52, 0x05, + 0x74, 0x72, 0x61, 0x63, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x0a, 0x74, 0x72, 0x61, 0x63, + 0x6b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x05, 0x48, 0x0e, 0x52, 0x0a, + 0x74, 0x72, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, + 0x04, 0x79, 0x65, 0x61, 0x72, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x48, 0x0f, 0x52, 0x04, 0x79, + 0x65, 0x61, 0x72, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x61, 0x6c, 0x62, 0x75, 0x6d, + 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x41, 0x72, 0x74, 0x69, 0x73, 0x74, + 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, + 0x62, 0x69, 0x74, 0x72, 0x61, 0x74, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x63, 0x6f, 0x6d, 0x70, + 0x6f, 0x73, 0x65, 0x72, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x63, 0x6f, 0x70, 0x79, 0x72, 0x69, + 0x67, 0x68, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x42, 0x0c, 0x0a, 0x0a, + 0x5f, 0x64, 0x69, 0x73, 0x63, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x67, 0x65, 0x6e, 0x72, + 0x65, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x68, 0x61, 0x73, 0x44, 0x72, 0x6d, 0x42, 0x14, 0x0a, 0x12, + 0x5f, 0x69, 0x73, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x69, 0x74, 0x72, 0x61, + 0x74, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x42, 0x08, 0x0a, 0x06, + 0x5f, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x74, 0x72, 0x61, 0x63, 0x6b, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x79, 0x65, 0x61, 0x72, 0x22, 0xb8, + 0x04, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x03, 0x72, 0x65, 0x66, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x63, 0x69, 0x73, 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, + 0x33, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x63, + 0x69, 0x73, 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, 0x40, 0x0a, + 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x23, 0x2e, 0x6f, 0x63, 0x69, 0x73, 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, 0x34, 0x0a, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6f, 0x63, 0x69, 0x73, 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, 0x22, 0x56, 0x0a, 0x05, 0x4d, 0x61, 0x74, + 0x63, 0x68, 0x12, 0x37, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x63, 0x69, 0x73, 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, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x76, 0x32, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63, + 0x69, 0x73, 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 ( @@ -426,26 +649,28 @@ func file_ocis_messages_search_v0_search_proto_rawDescGZIP() []byte { return file_ocis_messages_search_v0_search_proto_rawDescData } -var file_ocis_messages_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_ocis_messages_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_ocis_messages_search_v0_search_proto_goTypes = []interface{}{ (*ResourceID)(nil), // 0: ocis.messages.search.v0.ResourceID (*Reference)(nil), // 1: ocis.messages.search.v0.Reference - (*Entity)(nil), // 2: ocis.messages.search.v0.Entity - (*Match)(nil), // 3: ocis.messages.search.v0.Match - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp + (*Audio)(nil), // 2: ocis.messages.search.v0.Audio + (*Entity)(nil), // 3: ocis.messages.search.v0.Entity + (*Match)(nil), // 4: ocis.messages.search.v0.Match + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp } var file_ocis_messages_search_v0_search_proto_depIdxs = []int32{ 0, // 0: ocis.messages.search.v0.Reference.resource_id:type_name -> ocis.messages.search.v0.ResourceID 1, // 1: ocis.messages.search.v0.Entity.ref:type_name -> ocis.messages.search.v0.Reference 0, // 2: ocis.messages.search.v0.Entity.id:type_name -> ocis.messages.search.v0.ResourceID - 4, // 3: ocis.messages.search.v0.Entity.last_modified_time:type_name -> google.protobuf.Timestamp + 5, // 3: ocis.messages.search.v0.Entity.last_modified_time:type_name -> google.protobuf.Timestamp 0, // 4: ocis.messages.search.v0.Entity.parent_id:type_name -> ocis.messages.search.v0.ResourceID - 2, // 5: ocis.messages.search.v0.Match.entity:type_name -> ocis.messages.search.v0.Entity - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 2, // 5: ocis.messages.search.v0.Entity.audio:type_name -> ocis.messages.search.v0.Audio + 3, // 6: ocis.messages.search.v0.Match.entity:type_name -> ocis.messages.search.v0.Entity + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_ocis_messages_search_v0_search_proto_init() } @@ -479,7 +704,7 @@ func file_ocis_messages_search_v0_search_proto_init() { } } file_ocis_messages_search_v0_search_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Entity); i { + switch v := v.(*Audio); i { case 0: return &v.state case 1: @@ -491,6 +716,18 @@ func file_ocis_messages_search_v0_search_proto_init() { } } file_ocis_messages_search_v0_search_proto_msgTypes[3].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_ocis_messages_search_v0_search_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Match); i { case 0: return &v.state @@ -503,13 +740,14 @@ func file_ocis_messages_search_v0_search_proto_init() { } } } + file_ocis_messages_search_v0_search_proto_msgTypes[2].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_ocis_messages_search_v0_search_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 5, NumExtensions: 0, NumServices: 0, }, diff --git a/protogen/gen/ocis/messages/search/v0/search.pb.web.go b/protogen/gen/ocis/messages/search/v0/search.pb.web.go index 7055e8349..1ed7a0bf3 100644 --- a/protogen/gen/ocis/messages/search/v0/search.pb.web.go +++ b/protogen/gen/ocis/messages/search/v0/search.pb.web.go @@ -82,6 +82,42 @@ func (m *Reference) UnmarshalJSON(b []byte) error { var _ json.Unmarshaler = (*Reference)(nil) +// AudioJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of Audio. This struct is safe to replace or modify but +// should not be done so concurrently. +var AudioJSONMarshaler = 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 *Audio) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := AudioJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*Audio)(nil) + +// AudioJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of Audio. This struct is safe to replace or modify but +// should not be done so concurrently. +var AudioJSONUnmarshaler = 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 *Audio) UnmarshalJSON(b []byte) error { + return AudioJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*Audio)(nil) + // EntityJSONMarshaler describes the default jsonpb.Marshaler used by all // instances of Entity. This struct is safe to replace or modify but // should not be done so concurrently. diff --git a/protogen/gen/ocis/messages/settings/v0/settings.pb.go b/protogen/gen/ocis/messages/settings/v0/settings.pb.go index 9f0f3d92c..bae3a9c87 100644 --- a/protogen/gen/ocis/messages/settings/v0/settings.pb.go +++ b/protogen/gen/ocis/messages/settings/v0/settings.pb.go @@ -489,13 +489,13 @@ type Bundle struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // @gotags: yaml:"id" - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // @gotags: yaml:"name" - Type Bundle_Type `protobuf:"varint,3,opt,name=type,proto3,enum=ocis.messages.settings.v0.Bundle_Type" json:"type,omitempty"` // @gotags: yaml:"type" - Extension string `protobuf:"bytes,4,opt,name=extension,proto3" json:"extension,omitempty"` // @gotags: yaml:"extension" - DisplayName string `protobuf:"bytes,5,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // @gotags: yaml:"display_name" - Settings []*Setting `protobuf:"bytes,6,rep,name=settings,proto3" json:"settings,omitempty"` // @gotags: yaml:"settings" - Resource *Resource `protobuf:"bytes,7,opt,name=resource,proto3" json:"resource,omitempty"` // @gotags: yaml:"resource" + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" yaml:"id"` // @gotags: yaml:"id" + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty" yaml:"name"` // @gotags: yaml:"name" + Type Bundle_Type `protobuf:"varint,3,opt,name=type,proto3,enum=ocis.messages.settings.v0.Bundle_Type" json:"type,omitempty" yaml:"type"` // @gotags: yaml:"type" + Extension string `protobuf:"bytes,4,opt,name=extension,proto3" json:"extension,omitempty" yaml:"extension"` // @gotags: yaml:"extension" + DisplayName string `protobuf:"bytes,5,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty" yaml:"display_name"` // @gotags: yaml:"display_name" + Settings []*Setting `protobuf:"bytes,6,rep,name=settings,proto3" json:"settings,omitempty" yaml:"settings"` // @gotags: yaml:"settings" + Resource *Resource `protobuf:"bytes,7,opt,name=resource,proto3" json:"resource,omitempty" yaml:"resource"` // @gotags: yaml:"resource" } func (x *Bundle) Reset() { @@ -584,10 +584,10 @@ type Setting struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // @gotags: yaml:"id" - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // @gotags: yaml:"name" - DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // @gotags: yaml:"display_name" - Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` // @gotags: yaml:"description" + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" yaml:"id"` // @gotags: yaml:"id" + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty" yaml:"name"` // @gotags: yaml:"name" + DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty" yaml:"display_name"` // @gotags: yaml:"display_name" + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty" yaml:"description"` // @gotags: yaml:"description" // Types that are assignable to Value: // // *Setting_IntValue @@ -597,7 +597,7 @@ type Setting struct { // *Setting_MultiChoiceValue // *Setting_PermissionValue Value isSetting_Value `protobuf_oneof:"value"` - Resource *Resource `protobuf:"bytes,11,opt,name=resource,proto3" json:"resource,omitempty"` // @gotags: yaml:"resource" + Resource *Resource `protobuf:"bytes,11,opt,name=resource,proto3" json:"resource,omitempty" yaml:"resource"` // @gotags: yaml:"resource" } func (x *Setting) Reset() { @@ -721,27 +721,27 @@ type isSetting_Value interface { } type Setting_IntValue struct { - IntValue *Int `protobuf:"bytes,5,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value" + IntValue *Int `protobuf:"bytes,5,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value" } type Setting_StringValue struct { - StringValue *String `protobuf:"bytes,6,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value" + StringValue *String `protobuf:"bytes,6,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value" } type Setting_BoolValue struct { - BoolValue *Bool `protobuf:"bytes,7,opt,name=bool_value,json=boolValue,proto3,oneof"` // @gotags: yaml:"bool_value" + BoolValue *Bool `protobuf:"bytes,7,opt,name=bool_value,json=boolValue,proto3,oneof" yaml:"bool_value"` // @gotags: yaml:"bool_value" } type Setting_SingleChoiceValue struct { - SingleChoiceValue *SingleChoiceList `protobuf:"bytes,8,opt,name=single_choice_value,json=singleChoiceValue,proto3,oneof"` // @gotags: yaml:"single_choice_value" + SingleChoiceValue *SingleChoiceList `protobuf:"bytes,8,opt,name=single_choice_value,json=singleChoiceValue,proto3,oneof" yaml:"single_choice_value"` // @gotags: yaml:"single_choice_value" } type Setting_MultiChoiceValue struct { - MultiChoiceValue *MultiChoiceList `protobuf:"bytes,9,opt,name=multi_choice_value,json=multiChoiceValue,proto3,oneof"` // @gotags: yaml:"multi_choice_value" + MultiChoiceValue *MultiChoiceList `protobuf:"bytes,9,opt,name=multi_choice_value,json=multiChoiceValue,proto3,oneof" yaml:"multi_choice_value"` // @gotags: yaml:"multi_choice_value" } type Setting_PermissionValue struct { - PermissionValue *Permission `protobuf:"bytes,10,opt,name=permission_value,json=permissionValue,proto3,oneof"` // @gotags: yaml:"permission_value" + PermissionValue *Permission `protobuf:"bytes,10,opt,name=permission_value,json=permissionValue,proto3,oneof" yaml:"permission_value"` // @gotags: yaml:"permission_value" } func (*Setting_IntValue) isSetting_Value() {} @@ -761,11 +761,11 @@ type Int struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Default int64 `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty"` // @gotags: yaml:"default" - Min int64 `protobuf:"varint,2,opt,name=min,proto3" json:"min,omitempty"` // @gotags: yaml:"min" - Max int64 `protobuf:"varint,3,opt,name=max,proto3" json:"max,omitempty"` // @gotags: yaml:"max" - Step int64 `protobuf:"varint,4,opt,name=step,proto3" json:"step,omitempty"` // @gotags: yaml:"step" - Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty"` // @gotags: yaml:"placeholder" + Default int64 `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty" yaml:"default"` // @gotags: yaml:"default" + Min int64 `protobuf:"varint,2,opt,name=min,proto3" json:"min,omitempty" yaml:"min"` // @gotags: yaml:"min" + Max int64 `protobuf:"varint,3,opt,name=max,proto3" json:"max,omitempty" yaml:"max"` // @gotags: yaml:"max" + Step int64 `protobuf:"varint,4,opt,name=step,proto3" json:"step,omitempty" yaml:"step"` // @gotags: yaml:"step" + Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty" yaml:"placeholder"` // @gotags: yaml:"placeholder" } func (x *Int) Reset() { @@ -840,11 +840,11 @@ type String struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Default string `protobuf:"bytes,1,opt,name=default,proto3" json:"default,omitempty"` // @gotags: yaml:"default" - Required bool `protobuf:"varint,2,opt,name=required,proto3" json:"required,omitempty"` // @gotags: yaml:"required" - MinLength int32 `protobuf:"varint,3,opt,name=min_length,json=minLength,proto3" json:"min_length,omitempty"` // @gotags: yaml:"min_length" - MaxLength int32 `protobuf:"varint,4,opt,name=max_length,json=maxLength,proto3" json:"max_length,omitempty"` // @gotags: yaml:"max_length" - Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty"` // @gotags: yaml:"placeholder" + Default string `protobuf:"bytes,1,opt,name=default,proto3" json:"default,omitempty" yaml:"default"` // @gotags: yaml:"default" + Required bool `protobuf:"varint,2,opt,name=required,proto3" json:"required,omitempty" yaml:"required"` // @gotags: yaml:"required" + MinLength int32 `protobuf:"varint,3,opt,name=min_length,json=minLength,proto3" json:"min_length,omitempty" yaml:"min_length"` // @gotags: yaml:"min_length" + MaxLength int32 `protobuf:"varint,4,opt,name=max_length,json=maxLength,proto3" json:"max_length,omitempty" yaml:"max_length"` // @gotags: yaml:"max_length" + Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty" yaml:"placeholder"` // @gotags: yaml:"placeholder" } func (x *String) Reset() { @@ -919,8 +919,8 @@ type Bool struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Default bool `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty"` // @gotags: yaml:"default" - Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` // @gotags: yaml:"label" + Default bool `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty" yaml:"default"` // @gotags: yaml:"default" + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty" yaml:"label"` // @gotags: yaml:"label" } func (x *Bool) Reset() { @@ -974,7 +974,7 @@ type SingleChoiceList struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty"` // @gotags: yaml:"options" + Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty" yaml:"options"` // @gotags: yaml:"options" } func (x *SingleChoiceList) Reset() { @@ -1021,7 +1021,7 @@ type MultiChoiceList struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty"` // @gotags: yaml:"options" + Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty" yaml:"options"` // @gotags: yaml:"options" } func (x *MultiChoiceList) Reset() { @@ -1068,9 +1068,9 @@ type ListOption struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Value *ListOptionValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` // @gotags: yaml:"value" - Default bool `protobuf:"varint,2,opt,name=default,proto3" json:"default,omitempty"` // @gotags: yaml:"default" - DisplayValue string `protobuf:"bytes,3,opt,name=display_value,json=displayValue,proto3" json:"display_value,omitempty"` // @gotags: yaml:"display_value" + Value *ListOptionValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty" yaml:"value"` // @gotags: yaml:"value" + Default bool `protobuf:"varint,2,opt,name=default,proto3" json:"default,omitempty" yaml:"default"` // @gotags: yaml:"default" + DisplayValue string `protobuf:"bytes,3,opt,name=display_value,json=displayValue,proto3" json:"display_value,omitempty" yaml:"display_value"` // @gotags: yaml:"display_value" } func (x *ListOption) Reset() { @@ -1131,8 +1131,8 @@ type Permission struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Operation Permission_Operation `protobuf:"varint,1,opt,name=operation,proto3,enum=ocis.messages.settings.v0.Permission_Operation" json:"operation,omitempty"` // @gotags: yaml:"operation" - Constraint Permission_Constraint `protobuf:"varint,2,opt,name=constraint,proto3,enum=ocis.messages.settings.v0.Permission_Constraint" json:"constraint,omitempty"` // @gotags: yaml:"constraint" + Operation Permission_Operation `protobuf:"varint,1,opt,name=operation,proto3,enum=ocis.messages.settings.v0.Permission_Operation" json:"operation,omitempty" yaml:"operation"` // @gotags: yaml:"operation" + Constraint Permission_Constraint `protobuf:"varint,2,opt,name=constraint,proto3,enum=ocis.messages.settings.v0.Permission_Constraint" json:"constraint,omitempty" yaml:"constraint"` // @gotags: yaml:"constraint" } func (x *Permission) Reset() { @@ -1187,12 +1187,12 @@ type Value struct { unknownFields protoimpl.UnknownFields // id is the id of the Value. It is generated on saving it. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // @gotags: yaml:"id" - BundleId string `protobuf:"bytes,2,opt,name=bundle_id,json=bundleId,proto3" json:"bundle_id,omitempty"` // @gotags: yaml:"bundle_id" + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" yaml:"id"` // @gotags: yaml:"id" + BundleId string `protobuf:"bytes,2,opt,name=bundle_id,json=bundleId,proto3" json:"bundle_id,omitempty" yaml:"bundle_id"` // @gotags: yaml:"bundle_id" // setting_id is the id of the setting from within its bundle. - SettingId string `protobuf:"bytes,3,opt,name=setting_id,json=settingId,proto3" json:"setting_id,omitempty"` // @gotags: yaml:"setting_id" - AccountUuid string `protobuf:"bytes,4,opt,name=account_uuid,json=accountUuid,proto3" json:"account_uuid,omitempty"` // @gotags: yaml:"account_uuid" - Resource *Resource `protobuf:"bytes,5,opt,name=resource,proto3" json:"resource,omitempty"` // @gotags: yaml:"resource" + SettingId string `protobuf:"bytes,3,opt,name=setting_id,json=settingId,proto3" json:"setting_id,omitempty" yaml:"setting_id"` // @gotags: yaml:"setting_id" + AccountUuid string `protobuf:"bytes,4,opt,name=account_uuid,json=accountUuid,proto3" json:"account_uuid,omitempty" yaml:"account_uuid"` // @gotags: yaml:"account_uuid" + Resource *Resource `protobuf:"bytes,5,opt,name=resource,proto3" json:"resource,omitempty" yaml:"resource"` // @gotags: yaml:"resource" // Types that are assignable to Value: // // *Value_BoolValue @@ -1309,19 +1309,19 @@ type isValue_Value interface { } type Value_BoolValue struct { - BoolValue bool `protobuf:"varint,6,opt,name=bool_value,json=boolValue,proto3,oneof"` // @gotags: yaml:"bool_value" + BoolValue bool `protobuf:"varint,6,opt,name=bool_value,json=boolValue,proto3,oneof" yaml:"bool_value"` // @gotags: yaml:"bool_value" } type Value_IntValue struct { - IntValue int64 `protobuf:"varint,7,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value" + IntValue int64 `protobuf:"varint,7,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value" } type Value_StringValue struct { - StringValue string `protobuf:"bytes,8,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value" + StringValue string `protobuf:"bytes,8,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value" } type Value_ListValue struct { - ListValue *ListValue `protobuf:"bytes,9,opt,name=list_value,json=listValue,proto3,oneof"` // @gotags: yaml:"list_value" + ListValue *ListValue `protobuf:"bytes,9,opt,name=list_value,json=listValue,proto3,oneof" yaml:"list_value"` // @gotags: yaml:"list_value" } func (*Value_BoolValue) isValue_Value() {} @@ -1337,7 +1337,7 @@ type ListValue struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Values []*ListOptionValue `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` // @gotags: yaml:"values" + Values []*ListOptionValue `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty" yaml:"values"` // @gotags: yaml:"values" } func (x *ListValue) Reset() { @@ -1449,11 +1449,11 @@ type isListOptionValue_Option interface { } type ListOptionValue_StringValue struct { - StringValue string `protobuf:"bytes,1,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value" + StringValue string `protobuf:"bytes,1,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value" } type ListOptionValue_IntValue struct { - IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value" + IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value" } func (*ListOptionValue_StringValue) isListOptionValue_Option() {} diff --git a/protogen/gen/ocis/services/search/v0/search.swagger.json b/protogen/gen/ocis/services/search/v0/search.swagger.json index 267235414..6fce96faa 100644 --- a/protogen/gen/ocis/services/search/v0/search.swagger.json +++ b/protogen/gen/ocis/services/search/v0/search.swagger.json @@ -157,6 +157,66 @@ } } }, + "v0Audio": { + "type": "object", + "properties": { + "album": { + "type": "string" + }, + "albumArtist": { + "type": "string" + }, + "artist": { + "type": "string" + }, + "bitrate": { + "type": "string", + "format": "int64" + }, + "composers": { + "type": "string" + }, + "copyright": { + "type": "string" + }, + "disc": { + "type": "integer", + "format": "int32" + }, + "discCount": { + "type": "integer", + "format": "int32" + }, + "duration": { + "type": "string", + "format": "int64" + }, + "genre": { + "type": "string" + }, + "hasDrm": { + "type": "boolean" + }, + "isVariableBitrate": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "track": { + "type": "integer", + "format": "int32" + }, + "trackCount": { + "type": "integer", + "format": "int32" + }, + "year": { + "type": "integer", + "format": "int32" + } + } + }, "v0Entity": { "type": "object", "properties": { @@ -207,6 +267,9 @@ }, "highlights": { "type": "string" + }, + "audio": { + "$ref": "#/definitions/v0Audio" } } }, diff --git a/protogen/proto/ocis/messages/search/v0/search.proto b/protogen/proto/ocis/messages/search/v0/search.proto index 442eb502d..034d7f85e 100644 --- a/protogen/proto/ocis/messages/search/v0/search.proto +++ b/protogen/proto/ocis/messages/search/v0/search.proto @@ -17,6 +17,25 @@ message Reference { string path = 2; } +message Audio { + optional string album = 1; + optional string albumArtist = 2; + optional string artist = 3; + optional int64 bitrate = 4; + optional string composers = 5; + optional string copyright = 6; + optional int32 disc = 7; + optional int32 discCount = 8; + optional int64 duration = 9; + optional string genre = 10; + optional bool hasDrm = 11; + optional bool isVariableBitrate = 12; + optional string title = 13; + optional int32 track = 14; + optional int32 trackCount = 15; + optional int32 year = 16; +} + message Entity { Reference ref = 1; ResourceID id = 2; @@ -32,6 +51,7 @@ message Entity { ResourceID parent_id = 12; repeated string tags = 13; string highlights = 14; + Audio audio = 15; } message Match { diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 0e31bb677..322abfdb0 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -7,6 +7,9 @@ import ( "net/http" "net/url" "path" + "reflect" + "strconv" + "strings" "time" cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" @@ -17,6 +20,7 @@ import ( "github.com/cs3org/reva/v2/pkg/utils" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" "golang.org/x/crypto/sha3" ) @@ -87,7 +91,7 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { return } - files, err := formatDriveItems(lRes.Infos) + files, err := formatDriveItems(g.logger, lRes.Infos) if err != nil { g.logger.Error().Err(err).Msg("error encoding response as json") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) @@ -152,7 +156,7 @@ func (g Graph) GetDriveItem(w http.ResponseWriter, r *http.Request) { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) return } - driveItem, err := cs3ResourceToDriveItem(res.Info) + driveItem, err := cs3ResourceToDriveItem(g.logger, res.Info) if err != nil { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return @@ -220,7 +224,7 @@ func (g Graph) GetDriveItemChildren(w http.ResponseWriter, r *http.Request) { return } - files, err := formatDriveItems(res.Infos) + files, err := formatDriveItems(g.logger, res.Infos) if err != nil { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return @@ -244,7 +248,7 @@ func (g Graph) getDriveItem(ctx context.Context, ref storageprovider.Reference) refStr, _ := storagespace.FormatReference(&ref) return nil, fmt.Errorf("could not stat %s: %s", refStr, res.Status.Message) } - return cs3ResourceToDriveItem(res.Info) + return cs3ResourceToDriveItem(g.logger, res.Info) } func (g Graph) getRemoteItem(ctx context.Context, root *storageprovider.ResourceId, baseURL *url.URL) (*libregraph.RemoteItem, error) { @@ -285,10 +289,10 @@ func (g Graph) getRemoteItem(ctx context.Context, root *storageprovider.Resource return item, nil } -func formatDriveItems(mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) { +func formatDriveItems(logger *log.Logger, mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) { responses := make([]*libregraph.DriveItem, 0, len(mds)) for i := range mds { - res, err := cs3ResourceToDriveItem(mds[i]) + res, err := cs3ResourceToDriveItem(logger, mds[i]) if err != nil { return nil, err } @@ -302,7 +306,7 @@ func cs3TimestampToTime(t *types.Timestamp) time.Time { return time.Unix(int64(t.Seconds), int64(t.Nanos)) } -func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) { +func cs3ResourceToDriveItem(logger *log.Logger, res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) { size := new(int64) *size = int64(res.Size) // TODO lurking overflow: make size of libregraph drive item use uint64 @@ -330,9 +334,79 @@ func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.Driv if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER { driveItem.Folder = &libregraph.Folder{} } + + driveItem.Audio = cs3ResourceToDriveItemAudioFacet(logger, res) + return driveItem, nil } +func cs3ResourceToDriveItemAudioFacet(logger *log.Logger, res *storageprovider.ResourceInfo) *libregraph.Audio { + if !strings.HasPrefix(res.MimeType, "audio/") { + return nil + } + + k := res.ArbitraryMetadata.Metadata + if k == nil { + return nil + } + + var audio = &libregraph.Audio{} + if ok := unmarshalStringMap(logger, audio, k, "libre.graph.audio."); ok { + return audio + } + + return nil +} + +func getFieldName(structField reflect.StructField) string { + tag := structField.Tag.Get("json") + if tag == "" { + return structField.Name + } + + return strings.Split(tag, ",")[0] +} + +func unmarshalStringMap(logger *log.Logger, out any, flatMap map[string]string, prefix string) bool { + nonEmpty := false + obj := reflect.ValueOf(out).Elem() + for i := 0; i < obj.NumField(); i++ { + field := obj.Field(i) + structField := obj.Type().Field(i) + mapKey := prefix + getFieldName(structField) + + if value, ok := flatMap[mapKey]; ok { + if field.Kind() == reflect.Ptr { + newValue := reflect.New(field.Type().Elem()) + var tmp any + var err error + switch t := newValue.Type().Elem().Kind(); t { + case reflect.String: + tmp = value + case reflect.Int32: + tmp, err = strconv.ParseInt(value, 10, 32) + case reflect.Int64: + tmp, err = strconv.ParseInt(value, 10, 64) + case reflect.Bool: + tmp, err = strconv.ParseBool(value) + default: + err = errors.New("unsupported type") + logger.Error().Err(err).Str("type", t.String()).Str("mapKey", mapKey).Msg("target field type for value of mapKey is not supported") + } + if err != nil { + logger.Error().Err(err).Str("mapKey", mapKey).Msg("unmarshalling failed") + continue + } + newValue.Elem().Set(reflect.ValueOf(tmp).Convert(field.Type().Elem())) + field.Set(newValue) + nonEmpty = true + } + } + } + + return nonEmpty +} + func cs3ResourceToRemoteItem(res *storageprovider.ResourceInfo) (*libregraph.RemoteItem, error) { size := new(int64) *size = int64(res.Size) // TODO lurking overflow: make size of libregraph drive item use uint64 diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go index 8b307d433..c8f8ff208 100644 --- a/services/graph/pkg/service/v0/driveitems_test.go +++ b/services/graph/pkg/service/v0/driveitems_test.go @@ -240,38 +240,113 @@ var _ = Describe("Driveitems", func() { Expect(rr.Code).To(Equal(http.StatusInternalServerError)) }) - It("succeeds", func() { - mtime := time.Now() - gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ - Status: status.NewOK(ctx), - Infos: []*provider.ResourceInfo{ - { - Type: provider.ResourceType_RESOURCE_TYPE_FILE, - Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"}, - Etag: "etag", - Mtime: utils.TimeToTS(mtime), + Context("it succeeds", func() { + var ( + r *http.Request + mtime = time.Now() + ) + + BeforeEach(func() { + r = httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("driveID", "storageid$spaceid") + rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid") + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + }) + + assertItemsList := func(length int) itemsList { + svc.GetDriveItemChildren(rr, r) + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := itemsList{} + + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(res.Value)).To(Equal(1)) + Expect(res.Value[0].GetLastModifiedDateTime().Equal(mtime)).To(BeTrue()) + Expect(res.Value[0].GetETag()).To(Equal("etag")) + Expect(res.Value[0].GetId()).To(Equal("storageid$spaceid!opaqueid")) + Expect(res.Value[0].GetId()).To(Equal("storageid$spaceid!opaqueid")) + + return res + } + + It("returns a generic file", func() { + gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ + Status: status.NewOK(ctx), + Infos: []*provider.ResourceInfo{ + { + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"}, + Etag: "etag", + Mtime: utils.TimeToTS(mtime), + ArbitraryMetadata: nil, + }, }, - }, - }, nil) - r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children", nil) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("driveID", "storageid$spaceid") - rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid") - r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) - svc.GetDriveItemChildren(rr, r) - Expect(rr.Code).To(Equal(http.StatusOK)) - data, err := io.ReadAll(rr.Body) - Expect(err).ToNot(HaveOccurred()) + }, nil) - res := itemsList{} + res := assertItemsList(1) + Expect(res.Value[0].Audio).To(BeNil()) + }) - err = json.Unmarshal(data, &res) - Expect(err).ToNot(HaveOccurred()) + It("returns the audio facet if metadata is available", func() { + gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{ + Status: status.NewOK(ctx), + Infos: []*provider.ResourceInfo{ + { + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"}, + Etag: "etag", + Mtime: utils.TimeToTS(mtime), + MimeType: "audio/mpeg", + ArbitraryMetadata: &provider.ArbitraryMetadata{ + Metadata: map[string]string{ + "libre.graph.audio.album": "Some Album", + "libre.graph.audio.albumArtist": "Some AlbumArtist", + "libre.graph.audio.artist": "Some Artist", + "libre.graph.audio.bitrate": "192", + "libre.graph.audio.composers": "Some Composers", + "libre.graph.audio.copyright": "Some Copyright", + "libre.graph.audio.disc": "2", + "libre.graph.audio.discCount": "5", + "libre.graph.audio.duration": "225000", + "libre.graph.audio.genre": "Some Genre", + "libre.graph.audio.hasDrm": "false", + "libre.graph.audio.isVariableBitrate": "true", + "libre.graph.audio.title": "Some Title", + "libre.graph.audio.track": "6", + "libre.graph.audio.trackCount": "9", + "libre.graph.audio.year": "1994", + }, + }, + }, + }, + }, nil) - Expect(len(res.Value)).To(Equal(1)) - Expect(res.Value[0].GetLastModifiedDateTime().Equal(mtime)).To(BeTrue()) - Expect(res.Value[0].GetETag()).To(Equal("etag")) - Expect(res.Value[0].GetId()).To(Equal("storageid$spaceid!opaqueid")) + res := assertItemsList(1) + audio := res.Value[0].Audio + + Expect(audio).ToNot(BeNil()) + Expect(audio.Album).To(Equal(libregraph.PtrString("Some Album"))) + Expect(audio.AlbumArtist).To(Equal(libregraph.PtrString("Some AlbumArtist"))) + Expect(audio.Artist).To(Equal(libregraph.PtrString("Some Artist"))) + Expect(audio.Bitrate).To(Equal(libregraph.PtrInt64(192))) + Expect(audio.Composers).To(Equal(libregraph.PtrString("Some Composers"))) + Expect(audio.Copyright).To(Equal(libregraph.PtrString("Some Copyright"))) + Expect(audio.Disc).To(Equal(libregraph.PtrInt32(2))) + Expect(audio.DiscCount).To(Equal(libregraph.PtrInt32(5))) + Expect(audio.Duration).To(Equal(libregraph.PtrInt64(225000))) + Expect(audio.Genre).To(Equal(libregraph.PtrString("Some Genre"))) + Expect(audio.HasDrm).To(Equal(libregraph.PtrBool(false))) + Expect(audio.IsVariableBitrate).To(Equal(libregraph.PtrBool(true))) + Expect(audio.Title).To(Equal(libregraph.PtrString("Some Title"))) + Expect(audio.Track).To(Equal(libregraph.PtrInt32(6))) + Expect(audio.TrackCount).To(Equal(libregraph.PtrInt32(9))) + Expect(audio.Year).To(Equal(libregraph.PtrInt32(1994))) + }) }) }) }) diff --git a/services/search/pkg/content/content.go b/services/search/pkg/content/content.go index ef334e070..fccd80628 100644 --- a/services/search/pkg/content/content.go +++ b/services/search/pkg/content/content.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/bbalet/stopwords" + libregraph "github.com/owncloud/libre-graph-api-go" ) func init() { @@ -20,6 +21,7 @@ type Document struct { Mtime string MimeType string Tags []string + Audio *libregraph.Audio `json:"audio,omitempty"` } func CleanString(content, langCode string) string { diff --git a/services/search/pkg/content/tika.go b/services/search/pkg/content/tika.go index b2953170b..b169de8fd 100644 --- a/services/search/pkg/content/tika.go +++ b/services/search/pkg/content/tika.go @@ -3,6 +3,7 @@ package content import ( "context" "fmt" + "strconv" "strings" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -10,6 +11,7 @@ import ( "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/google/go-tika/tika" + libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/search/pkg/config" ) @@ -86,6 +88,68 @@ 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)) } + + if contentType, err := getFirstValue(meta, "Content-Type"); err == nil && strings.HasPrefix(contentType, "audio/") { + audio := libregraph.Audio{} + + if v, err := getFirstValue(meta, "xmpDM:album"); err == nil { + audio.SetAlbum(v) + } + + if v, err := getFirstValue(meta, "xmpDM:albumArtist"); err == nil { + audio.SetAlbumArtist(v) + } + + if v, err := getFirstValue(meta, "xmpDM:artist"); err == nil { + audio.SetArtist(v) + } + + // TODO: audio.Bitrate: not provided by tika + // TODO: audio.Composers: not provided by tika + // TODO: audio.Copyright: not provided by tika for audio files? + + if v, err := getFirstValue(meta, "xmpDM:discNumber"); err == nil { + if i, err := strconv.ParseInt(v, 10, 32); err == nil { + audio.SetDisc(int32(i)) + } + + } + + // TODO: audio.DiscCount: not provided by tika + + if v, err := getFirstValue(meta, "xmpDM:duration"); err == nil { + if i, err := strconv.ParseInt(v, 10, 64); err == nil { + audio.SetDuration(i * 1000) + } + } + + if v, err := getFirstValue(meta, "xmpDM:genre"); err == nil { + audio.SetGenre(v) + } + + // TODO: audio.HasDrm: not provided by tika + // TODO: audio.IsVariableBitrate: not provided by tika + + if v, err := getFirstValue(meta, "dc:title"); err == nil { + audio.SetTitle(v) + } + + if v, err := getFirstValue(meta, "xmpDM:trackNumber"); err == nil { + if i, err := strconv.ParseInt(v, 10, 32); err == nil { + audio.SetTrack(int32(i)) + } + } + + // TODO: audio.TrackCount: not provided by tika + + if v, err := getFirstValue(meta, "xmpDM:releaseDate"); err == nil { + if i, err := strconv.ParseInt(v, 10, 32); err == nil { + audio.SetYear(int32(i)) + } + } + + doc.Audio = &audio + } } if langCode, _ := t.tika.LanguageString(ctx, doc.Content); langCode != "" && t.CleanStopWords { diff --git a/services/search/pkg/content/tika_test.go b/services/search/pkg/content/tika_test.go index fbb5c0578..497247cac 100644 --- a/services/search/pkg/content/tika_test.go +++ b/services/search/pkg/content/tika_test.go @@ -13,6 +13,7 @@ import ( . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" + libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/v2/ocis-pkg/log" conf "github.com/owncloud/ocis/v2/services/search/pkg/config/defaults" "github.com/owncloud/ocis/v2/services/search/pkg/content" @@ -22,17 +23,19 @@ import ( var _ = Describe("Tika", func() { Describe("extract", func() { var ( - body string - language string - version string - srv *httptest.Server - tika *content.Tika + body string + fullResponse string + language string + version string + srv *httptest.Server + tika *content.Tika ) BeforeEach(func() { body = "" language = "" version = "" + fullResponse = "" srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { out := "" switch req.URL.Path { @@ -41,7 +44,11 @@ var _ = Describe("Tika", func() { case "/language/string": out = language case "/rmeta/text": - out = fmt.Sprintf(`[{"X-TIKA:content":"%s"}]`, body) + if fullResponse != "" { + out = fullResponse + } else { + out = fmt.Sprintf(`[{"X-TIKA:content":"%s"}]`, body) + } } _, _ = w.Write([]byte(out)) @@ -83,6 +90,56 @@ var _ = Describe("Tika", func() { Expect(doc.Content).To(Equal(body)) }) + It("adds audio content", func() { + fullResponse = `[ + { + "xmpDM:genre": "Some Genre", + "xmpDM:album": "Some Album", + "xmpDM:trackNumber": "7", + "xmpDM:discNumber": "4", + "xmpDM:releaseDate": "2004", + "xmpDM:artist": "Some Artist", + "xmpDM:albumArtist": "Some AlbumArtist", + "xmpDM:audioCompressor": "MP3", + "xmpDM:audioChannelType": "Stereo", + "version": "MPEG 3 Layer III Version 1", + "xmpDM:logComment": "some comment", + "xmpDM:audioSampleRate": "44100", + "channels": "2", + "dc:title": "Some Title", + "xmpDM:duration": "225", + "Content-Type": "audio/mpeg", + "samplerate": "44100" + } + ]` + doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Size: 1, + }) + Expect(err).ToNot(HaveOccurred()) + + audio := doc.Audio + Expect(audio).ToNot(BeNil()) + + Expect(audio.Album).To(Equal(libregraph.PtrString("Some Album"))) + Expect(audio.AlbumArtist).To(Equal(libregraph.PtrString("Some AlbumArtist"))) + Expect(audio.Artist).To(Equal(libregraph.PtrString("Some Artist"))) + // Expect(audio.Bitrate).To(Equal(libregraph.PtrInt64(192))) + // Expect(audio.Composers).To(Equal(libregraph.PtrString("Some Composers"))) + // Expect(audio.Copyright).To(Equal(libregraph.PtrString("Some Copyright"))) + Expect(audio.Disc).To(Equal(libregraph.PtrInt32(4))) + // Expect(audio.DiscCount).To(Equal(libregraph.PtrInt32(5))) + Expect(audio.Duration).To(Equal(libregraph.PtrInt64(225000))) + Expect(audio.Genre).To(Equal(libregraph.PtrString("Some Genre"))) + // Expect(audio.HasDrm).To(Equal(libregraph.PtrBool(false))) + // Expect(audio.IsVariableBitrate).To(Equal(libregraph.PtrBool(true))) + Expect(audio.Title).To(Equal(libregraph.PtrString("Some Title"))) + Expect(audio.Track).To(Equal(libregraph.PtrInt32(7))) + // Expect(audio.TrackCount).To(Equal(libregraph.PtrInt32(9))) + Expect(audio.Year).To(Equal(libregraph.PtrInt32(2004))) + + }) + It("removes stop words", func() { body = "body to test stop words!!! against almost everyone" language = "en" diff --git a/services/search/pkg/engine/bleve.go b/services/search/pkg/engine/bleve.go index c4ad66c6d..8e1b6efed 100644 --- a/services/search/pkg/engine/bleve.go +++ b/services/search/pkg/engine/bleve.go @@ -6,6 +6,7 @@ import ( "math" "path" "path/filepath" + "reflect" "strings" "time" @@ -24,6 +25,7 @@ import ( "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" + libregraph "github.com/owncloud/libre-graph-api-go" searchMessage "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0" searchService "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0" "github.com/owncloud/ocis/v2/services/search/pkg/content" @@ -208,6 +210,7 @@ func (b *Bleve) Search(ctx context.Context, sir *searchService.SearchIndexReques Deleted: getFieldValue[bool](hit.Fields, "Deleted"), Tags: getFieldSliceValue[string](hit.Fields, "Tags"), Highlights: getFragmentValue(hit.Fragments, "Content", 0), + Audio: getAudioValue[searchMessage.Audio](hit.Fields), }, } @@ -325,10 +328,60 @@ func (b *Bleve) getResource(id string) (*Resource, error) { MimeType: getFieldValue[string](fields, "MimeType"), Content: getFieldValue[string](fields, "Content"), Tags: getFieldSliceValue[string](fields, "Tags"), + Audio: getAudioValue[libregraph.Audio](fields), }, }, nil } +func newPointerOfType[T any]() *T { + t := reflect.TypeOf((*T)(nil)).Elem() + ptr := reflect.New(t).Interface() + return ptr.(*T) +} + +func getFieldName(structField reflect.StructField) string { + tag := structField.Tag.Get("json") + if tag == "" { + return structField.Name + } + + return strings.Split(tag, ",")[0] +} + +func unmarshalInterfaceMap(out any, flatMap map[string]interface{}, prefix string) bool { + nonEmpty := false + obj := reflect.ValueOf(out).Elem() + for i := 0; i < obj.NumField(); i++ { + field := obj.Field(i) + structField := obj.Type().Field(i) + mapKey := prefix + getFieldName(structField) + + if value, ok := flatMap[mapKey]; ok { + if field.Kind() == reflect.Ptr { + alloc := reflect.New(field.Type().Elem()) + alloc.Elem().Set(reflect.ValueOf(value).Convert(field.Type().Elem())) + field.Set(alloc) + nonEmpty = true + } + } + } + + return nonEmpty +} + +func getAudioValue[T any](fields map[string]interface{}) *T { + if !strings.HasPrefix(getFieldValue[string](fields, "MimeType"), "audio/") { + return nil + } + + var audio = newPointerOfType[T]() + if ok := unmarshalInterfaceMap(audio, fields, "audio."); ok { + return audio + } + + return nil +} + func (b *Bleve) updateEntity(id string, mutateFunc func(r *Resource)) (*Resource, error) { it, err := b.getResource(id) if err != nil { diff --git a/services/search/pkg/engine/bleve_test.go b/services/search/pkg/engine/bleve_test.go index 53690a81d..8b9668faf 100644 --- a/services/search/pkg/engine/bleve_test.go +++ b/services/search/pkg/engine/bleve_test.go @@ -10,6 +10,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + libregraph "github.com/owncloud/libre-graph-api-go" searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0" searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0" "github.com/owncloud/ocis/v2/services/search/pkg/content" @@ -352,6 +353,7 @@ var _ = Describe("Bleve", func() { Expect(res.TotalMatches).To(Equal(int32(1))) }) }) + }) Describe("Upsert", func() { @@ -483,4 +485,68 @@ var _ = Describe("Bleve", func() { }) }) + + Describe("File type specific metadata", func() { + + Context("with audio metadata", func() { + BeforeEach(func() { + resource := engine.Resource{ + ID: "1$2!7", + ParentID: rootResource.ID, + RootID: rootResource.ID, + Path: "./some_song.mp3", + Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), + Document: content.Document{ + Name: "some_song.mp3", + MimeType: "audio/mpeg", + Audio: &libregraph.Audio{ + Album: libregraph.PtrString("Some Album"), + AlbumArtist: libregraph.PtrString("Some AlbumArtist"), + Artist: libregraph.PtrString("Some Artist"), + Bitrate: libregraph.PtrInt64(192), + Composers: libregraph.PtrString("Some Composers"), + Copyright: libregraph.PtrString(""), + Disc: libregraph.PtrInt32(2), + DiscCount: libregraph.PtrInt32(5), + Duration: libregraph.PtrInt64(225000), + Genre: libregraph.PtrString("Some Genre"), + HasDrm: libregraph.PtrBool(false), + IsVariableBitrate: libregraph.PtrBool(true), + Title: libregraph.PtrString("Some Title"), + Track: libregraph.PtrInt32(34), + TrackCount: libregraph.PtrInt32(99), + Year: libregraph.PtrInt32(2004), + }, + }, + } + err := eng.Upsert(resource.ID, resource) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns audio metadata for search", func() { + matches := assertDocCount(rootResource.ID, `*song*`, 1) + audio := matches[0].Entity.Audio + + Expect(audio).ToNot(BeNil()) + + Expect(audio.Album).To(Equal(libregraph.PtrString("Some Album"))) + Expect(audio.AlbumArtist).To(Equal(libregraph.PtrString("Some AlbumArtist"))) + Expect(audio.Artist).To(Equal(libregraph.PtrString("Some Artist"))) + Expect(audio.Bitrate).To(Equal(libregraph.PtrInt64(192))) + Expect(audio.Composers).To(Equal(libregraph.PtrString("Some Composers"))) + Expect(audio.Copyright).To(Equal(libregraph.PtrString(""))) + Expect(audio.Disc).To(Equal(libregraph.PtrInt32(2))) + Expect(audio.DiscCount).To(Equal(libregraph.PtrInt32(5))) + Expect(audio.Duration).To(Equal(libregraph.PtrInt64(225000))) + Expect(audio.Genre).To(Equal(libregraph.PtrString("Some Genre"))) + Expect(audio.HasDrm).To(Equal(libregraph.PtrBool(false))) + Expect(audio.IsVariableBitrate).To(Equal(libregraph.PtrBool(true))) + Expect(audio.Title).To(Equal(libregraph.PtrString("Some Title"))) + Expect(audio.Track).To(Equal(libregraph.PtrInt32(34))) + Expect(audio.TrackCount).To(Equal(libregraph.PtrInt32(99))) + Expect(audio.Year).To(Equal(libregraph.PtrInt32(2004))) + }) + }) + + }) }) diff --git a/services/search/pkg/search/service.go b/services/search/pkg/search/service.go index 8b8ee1f0c..199b3df31 100644 --- a/services/search/pkg/search/service.go +++ b/services/search/pkg/search/service.go @@ -5,11 +5,13 @@ import ( "fmt" "path/filepath" "sort" + "strconv" "strings" "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" revactx "github.com/cs3org/reva/v2/pkg/ctx" @@ -22,6 +24,7 @@ import ( "golang.org/x/sync/errgroup" "google.golang.org/protobuf/types/known/fieldmaskpb" + libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/v2/ocis-pkg/log" searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0" searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0" @@ -487,6 +490,70 @@ func (s *Service) UpsertItem(ref *provider.Reference, uID *user.UserId) { } else { logDocCount(s.engine, s.logger) } + + // determine if metadata needs to be stored in storage as well + metadata := map[string]string{} + addAudioMetadata(metadata, doc.Audio) + if len(metadata) == 0 { + return + } + + s.logger.Trace().Str("name", doc.Name).Interface("metadata", metadata).Msg("Storing metadata") + + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + s.logger.Error().Err(err).Msg("could not retrieve client to store metadata") + return + } + + resp, err := gatewayClient.SetArbitraryMetadata(ctx, &provider.SetArbitraryMetadataRequest{ + Ref: ref, + ArbitraryMetadata: &provider.ArbitraryMetadata{ + Metadata: metadata, + }, + }) + if err != nil || resp.GetStatus().GetCode() != rpc.Code_CODE_OK { + s.logger.Error().Err(err).Int32("status", int32(resp.GetStatus().GetCode())).Msg("error storing metadata") + return + } +} + +func addAudioMetadata(metadata map[string]string, audio *libregraph.Audio) { + if audio == nil { + return + } + marshalToStringMap(audio, metadata, "libre.graph.audio.") +} + +func marshalToStringMap[T libregraph.MappedNullable](source T, target map[string]string, prefix string) { + // ToMap never returns a non-nil error ... + m, _ := source.ToMap() + + for k, v := range m { + if v == nil { + continue + } + target[prefix+k] = valueToString(v) + } +} + +func valueToString(value interface{}) string { + if value == nil { + return "" + } + + switch v := value.(type) { + case *string: + return *v + case *int32: + return strconv.FormatInt(int64(*v), 10) + case *int64: + return strconv.FormatInt(*v, 10) + case *bool: + return strconv.FormatBool(*v) + default: + return fmt.Sprintf("%v", v) + } } // RestoreItem makes the item available again.