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
This commit is contained in:
Dominik Schmidt
2023-11-06 08:56:46 +01:00
committed by GitHub
parent 418e13517d
commit db32fb46f9
14 changed files with 974 additions and 153 deletions
+81 -7
View File
@@ -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
+103 -28
View File
@@ -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)))
})
})
})
})
+2
View File
@@ -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 {
+64
View File
@@ -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 {
+63 -6
View File
@@ -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"
+53
View File
@@ -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 {
+66
View File
@@ -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)))
})
})
})
})
+67
View File
@@ -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.