navidrome/plugins/adapter_scrobbler.go
Kendall Garner 0cd15c1ddc
feat(prometheus): add metrics to Subsonic API and Plugins (#4266)
* Add prometheus metrics to subsonic and plugins

* address feedback, do not log error if operation is not supported

* add missing timestamp and client to stats

* remove .view from subsonic route

* directly inject DataStore in Prometheus, to avoid having to pass it in every call

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
2025-06-27 22:13:57 -04:00

155 lines
4.4 KiB
Go

package plugins
import (
"context"
"time"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/plugins/api"
"github.com/tetratelabs/wazero"
)
func newWasmScrobblerPlugin(wasmPath, pluginID string, m *Manager, runtime api.WazeroNewRuntime, mc wazero.ModuleConfig) WasmPlugin {
loader, err := api.NewScrobblerPlugin(context.Background(), api.WazeroRuntime(runtime), api.WazeroModuleConfig(mc))
if err != nil {
log.Error("Error creating scrobbler service plugin", "plugin", pluginID, "path", wasmPath, err)
return nil
}
return &wasmScrobblerPlugin{
wasmBasePlugin: newWasmBasePlugin[api.Scrobbler, *api.ScrobblerPlugin](
wasmPath,
pluginID,
CapabilityScrobbler,
m.metrics,
loader,
func(ctx context.Context, l *api.ScrobblerPlugin, path string) (api.Scrobbler, error) {
return l.Load(ctx, path)
},
),
}
}
type wasmScrobblerPlugin struct {
*wasmBasePlugin[api.Scrobbler, *api.ScrobblerPlugin]
}
func (w *wasmScrobblerPlugin) IsAuthorized(ctx context.Context, userId string) bool {
username, _ := request.UsernameFrom(ctx)
if username == "" {
u, ok := request.UserFrom(ctx)
if ok {
username = u.UserName
}
}
result, err := callMethod(ctx, w, "IsAuthorized", func(inst api.Scrobbler) (bool, error) {
resp, err := inst.IsAuthorized(ctx, &api.ScrobblerIsAuthorizedRequest{
UserId: userId,
Username: username,
})
if err != nil {
return false, err
}
if resp.Error != "" {
return false, nil
}
return resp.Authorized, nil
})
return err == nil && result
}
func (w *wasmScrobblerPlugin) NowPlaying(ctx context.Context, userId string, track *model.MediaFile, position int) error {
username, _ := request.UsernameFrom(ctx)
if username == "" {
u, ok := request.UserFrom(ctx)
if ok {
username = u.UserName
}
}
artists := make([]*api.Artist, 0, len(track.Participants[model.RoleArtist]))
for _, a := range track.Participants[model.RoleArtist] {
artists = append(artists, &api.Artist{Name: a.Name, Mbid: a.MbzArtistID})
}
albumArtists := make([]*api.Artist, 0, len(track.Participants[model.RoleAlbumArtist]))
for _, a := range track.Participants[model.RoleAlbumArtist] {
albumArtists = append(albumArtists, &api.Artist{Name: a.Name, Mbid: a.MbzArtistID})
}
trackInfo := &api.TrackInfo{
Id: track.ID,
Mbid: track.MbzRecordingID,
Name: track.Title,
Album: track.Album,
AlbumMbid: track.MbzAlbumID,
Artists: artists,
AlbumArtists: albumArtists,
Length: int32(track.Duration),
Position: int32(position),
}
_, err := callMethod(ctx, w, "NowPlaying", func(inst api.Scrobbler) (struct{}, error) {
resp, err := inst.NowPlaying(ctx, &api.ScrobblerNowPlayingRequest{
UserId: userId,
Username: username,
Track: trackInfo,
Timestamp: time.Now().Unix(),
})
if err != nil {
return struct{}{}, err
}
if resp.Error != "" {
return struct{}{}, nil
}
return struct{}{}, nil
})
return err
}
func (w *wasmScrobblerPlugin) Scrobble(ctx context.Context, userId string, s scrobbler.Scrobble) error {
username, _ := request.UsernameFrom(ctx)
if username == "" {
u, ok := request.UserFrom(ctx)
if ok {
username = u.UserName
}
}
track := &s.MediaFile
artists := make([]*api.Artist, 0, len(track.Participants[model.RoleArtist]))
for _, a := range track.Participants[model.RoleArtist] {
artists = append(artists, &api.Artist{Name: a.Name, Mbid: a.MbzArtistID})
}
albumArtists := make([]*api.Artist, 0, len(track.Participants[model.RoleAlbumArtist]))
for _, a := range track.Participants[model.RoleAlbumArtist] {
albumArtists = append(albumArtists, &api.Artist{Name: a.Name, Mbid: a.MbzArtistID})
}
trackInfo := &api.TrackInfo{
Id: track.ID,
Mbid: track.MbzRecordingID,
Name: track.Title,
Album: track.Album,
AlbumMbid: track.MbzAlbumID,
Artists: artists,
AlbumArtists: albumArtists,
Length: int32(track.Duration),
}
_, err := callMethod(ctx, w, "Scrobble", func(inst api.Scrobbler) (struct{}, error) {
resp, err := inst.Scrobble(ctx, &api.ScrobblerScrobbleRequest{
UserId: userId,
Username: username,
Track: trackInfo,
Timestamp: s.TimeStamp.Unix(),
})
if err != nil {
return struct{}{}, err
}
if resp.Error != "" {
return struct{}{}, nil
}
return struct{}{}, nil
})
return err
}