package plugins import ( "context" "github.com/navidrome/navidrome/core/agents" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/plugins/api" "github.com/tetratelabs/wazero" ) // NewWasmMediaAgent creates a new adapter for a MetadataAgent plugin func newWasmMediaAgent(wasmPath, pluginID string, m *Manager, runtime api.WazeroNewRuntime, mc wazero.ModuleConfig) WasmPlugin { loader, err := api.NewMetadataAgentPlugin(context.Background(), api.WazeroRuntime(runtime), api.WazeroModuleConfig(mc)) if err != nil { log.Error("Error creating media metadata service plugin", "plugin", pluginID, "path", wasmPath, err) return nil } return &wasmMediaAgent{ wasmBasePlugin: newWasmBasePlugin[api.MetadataAgent, *api.MetadataAgentPlugin]( wasmPath, pluginID, CapabilityMetadataAgent, m.metrics, loader, func(ctx context.Context, l *api.MetadataAgentPlugin, path string) (api.MetadataAgent, error) { return l.Load(ctx, path) }, ), } } // wasmMediaAgent adapts a MetadataAgent plugin to implement the agents.Interface type wasmMediaAgent struct { *wasmBasePlugin[api.MetadataAgent, *api.MetadataAgentPlugin] } func (w *wasmMediaAgent) AgentName() string { return w.id } func (w *wasmMediaAgent) mapError(err error) error { if err != nil && (err.Error() == api.ErrNotFound.Error() || err.Error() == api.ErrNotImplemented.Error()) { return agents.ErrNotFound } return err } // Album-related methods func (w *wasmMediaAgent) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*agents.AlbumInfo, error) { return callMethod(ctx, w, "GetAlbumInfo", func(inst api.MetadataAgent) (*agents.AlbumInfo, error) { res, err := inst.GetAlbumInfo(ctx, &api.AlbumInfoRequest{Name: name, Artist: artist, Mbid: mbid}) if err != nil { return nil, w.mapError(err) } if res == nil || res.Info == nil { return nil, agents.ErrNotFound } info := res.Info return &agents.AlbumInfo{ Name: info.Name, MBID: info.Mbid, Description: info.Description, URL: info.Url, }, nil }) } func (w *wasmMediaAgent) GetAlbumImages(ctx context.Context, name, artist, mbid string) ([]agents.ExternalImage, error) { return callMethod(ctx, w, "GetAlbumImages", func(inst api.MetadataAgent) ([]agents.ExternalImage, error) { res, err := inst.GetAlbumImages(ctx, &api.AlbumImagesRequest{Name: name, Artist: artist, Mbid: mbid}) if err != nil { return nil, w.mapError(err) } return convertExternalImages(res.Images), nil }) } // Artist-related methods func (w *wasmMediaAgent) GetArtistMBID(ctx context.Context, id string, name string) (string, error) { return callMethod(ctx, w, "GetArtistMBID", func(inst api.MetadataAgent) (string, error) { res, err := inst.GetArtistMBID(ctx, &api.ArtistMBIDRequest{Id: id, Name: name}) if err != nil { return "", w.mapError(err) } return res.GetMbid(), nil }) } func (w *wasmMediaAgent) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) { return callMethod(ctx, w, "GetArtistURL", func(inst api.MetadataAgent) (string, error) { res, err := inst.GetArtistURL(ctx, &api.ArtistURLRequest{Id: id, Name: name, Mbid: mbid}) if err != nil { return "", w.mapError(err) } return res.GetUrl(), nil }) } func (w *wasmMediaAgent) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) { return callMethod(ctx, w, "GetArtistBiography", func(inst api.MetadataAgent) (string, error) { res, err := inst.GetArtistBiography(ctx, &api.ArtistBiographyRequest{Id: id, Name: name, Mbid: mbid}) if err != nil { return "", w.mapError(err) } return res.GetBiography(), nil }) } func (w *wasmMediaAgent) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) { return callMethod(ctx, w, "GetSimilarArtists", func(inst api.MetadataAgent) ([]agents.Artist, error) { resp, err := inst.GetSimilarArtists(ctx, &api.ArtistSimilarRequest{Id: id, Name: name, Mbid: mbid, Limit: int32(limit)}) if err != nil { return nil, w.mapError(err) } artists := make([]agents.Artist, 0, len(resp.GetArtists())) for _, a := range resp.GetArtists() { artists = append(artists, agents.Artist{ Name: a.GetName(), MBID: a.GetMbid(), }) } return artists, nil }) } func (w *wasmMediaAgent) GetArtistImages(ctx context.Context, id, name, mbid string) ([]agents.ExternalImage, error) { return callMethod(ctx, w, "GetArtistImages", func(inst api.MetadataAgent) ([]agents.ExternalImage, error) { res, err := inst.GetArtistImages(ctx, &api.ArtistImageRequest{Id: id, Name: name, Mbid: mbid}) if err != nil { return nil, w.mapError(err) } return convertExternalImages(res.Images), nil }) } func (w *wasmMediaAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) { return callMethod(ctx, w, "GetArtistTopSongs", func(inst api.MetadataAgent) ([]agents.Song, error) { resp, err := inst.GetArtistTopSongs(ctx, &api.ArtistTopSongsRequest{Id: id, ArtistName: artistName, Mbid: mbid, Count: int32(count)}) if err != nil { return nil, w.mapError(err) } songs := make([]agents.Song, 0, len(resp.GetSongs())) for _, s := range resp.GetSongs() { songs = append(songs, agents.Song{ Name: s.GetName(), MBID: s.GetMbid(), }) } return songs, nil }) } // Helper function to convert ExternalImage objects from the API to the agents package func convertExternalImages(images []*api.ExternalImage) []agents.ExternalImage { result := make([]agents.ExternalImage, 0, len(images)) for _, img := range images { result = append(result, agents.ExternalImage{ URL: img.GetUrl(), Size: int(img.GetSize()), }) } return result }