mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
initial api, some testing
This commit is contained in:
parent
1a8d3e2b82
commit
23d60337b8
38
plugins/host/scrobble_retriever.go
Normal file
38
plugins/host/scrobble_retriever.go
Normal file
@ -0,0 +1,38 @@
|
||||
package host
|
||||
|
||||
import "context"
|
||||
|
||||
type ScrobbleList struct {
|
||||
Scrobbles []ScrobbleRef `json:"scrobbles"`
|
||||
NextTimestamp *int64 `json:"nextTimestamp,omitempty"`
|
||||
}
|
||||
|
||||
type ScrobbleRef struct {
|
||||
ID int64 `json:"id"`
|
||||
MediaFileID string `json:"mediaFileId"`
|
||||
SubmissionTime int64 `json:"submissionTime"`
|
||||
}
|
||||
|
||||
type ScrobbleOptions struct {
|
||||
FromTimestamp *int64 `json:"fromTimestamp,omitempty"`
|
||||
ToTimestamp *int64 `json:"toTimestamp,omitempty"`
|
||||
MaxItems int `json:"maxItems"`
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverService allows a plugin to retrieve scrobbles for one or more authorized users.
|
||||
// It will only provide the media_file ID and submission time, which can be combined with the MatcherService
|
||||
// to fetch deduped tracks
|
||||
//
|
||||
//nd:hostservice name=ScrobbleRetriever permission=scrobbleRetriever
|
||||
type ScrobbleRetrieverService interface {
|
||||
// GetFirstTimestamp returns the unix timestamp of the oldest scrobble for the user
|
||||
//nd:hostfunc
|
||||
GetFirstTimestamp(ctx context.Context, username string) (*int64, error)
|
||||
|
||||
// GetLastTimestamp returns the unix timestamp of the most recent scrobble for the user
|
||||
//nd:hostfunc
|
||||
GetLastTimestamp(ctx context.Context, username string) (*int64, error)
|
||||
|
||||
//nd:hostfunc
|
||||
GetScrobbles(ctx context.Context, username string, options ScrobbleOptions) (*ScrobbleList, error)
|
||||
}
|
||||
181
plugins/host/scrobbleretriever_gen.go
Normal file
181
plugins/host/scrobbleretriever_gen.go
Normal file
@ -0,0 +1,181 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
extism "github.com/extism/go-sdk"
|
||||
)
|
||||
|
||||
// ScrobbleRetrieverGetLastTimestampRequest is the request type for ScrobbleRetriever.GetLastTimestamp.
|
||||
type ScrobbleRetrieverGetLastTimestampRequest struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetLastTimestampResponse is the response type for ScrobbleRetriever.GetLastTimestamp.
|
||||
type ScrobbleRetrieverGetLastTimestampResponse struct {
|
||||
Result *int64 `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetFirstTimestampRequest is the request type for ScrobbleRetriever.GetFirstTimestamp.
|
||||
type ScrobbleRetrieverGetFirstTimestampRequest struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetFirstTimestampResponse is the response type for ScrobbleRetriever.GetFirstTimestamp.
|
||||
type ScrobbleRetrieverGetFirstTimestampResponse struct {
|
||||
Result *int64 `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetScrobblesRequest is the request type for ScrobbleRetriever.GetScrobbles.
|
||||
type ScrobbleRetrieverGetScrobblesRequest struct {
|
||||
Username string `json:"username"`
|
||||
Options ScrobbleOptions `json:"options"`
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetScrobblesResponse is the response type for ScrobbleRetriever.GetScrobbles.
|
||||
type ScrobbleRetrieverGetScrobblesResponse struct {
|
||||
Result *ScrobbleList `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterScrobbleRetrieverHostFunctions registers ScrobbleRetriever service host functions.
|
||||
// The returned host functions should be added to the plugin's configuration.
|
||||
func RegisterScrobbleRetrieverHostFunctions(service ScrobbleRetrieverService) []extism.HostFunction {
|
||||
return []extism.HostFunction{
|
||||
newScrobbleRetrieverGetLastTimestampHostFunction(service),
|
||||
newScrobbleRetrieverGetFirstTimestampHostFunction(service),
|
||||
newScrobbleRetrieverGetScrobblesHostFunction(service),
|
||||
}
|
||||
}
|
||||
|
||||
func newScrobbleRetrieverGetLastTimestampHostFunction(service ScrobbleRetrieverService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"scrobbleretriever_getlasttimestamp",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read JSON request from plugin memory
|
||||
reqBytes, err := p.ReadBytes(stack[0])
|
||||
if err != nil {
|
||||
scrobbleretrieverWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
var req ScrobbleRetrieverGetLastTimestampRequest
|
||||
if err := json.Unmarshal(reqBytes, &req); err != nil {
|
||||
scrobbleretrieverWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
result, svcErr := service.GetLastTimestamp(ctx, req.Username)
|
||||
if svcErr != nil {
|
||||
scrobbleretrieverWriteError(p, stack, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Write JSON response to plugin memory
|
||||
resp := ScrobbleRetrieverGetLastTimestampResponse{
|
||||
Result: result,
|
||||
}
|
||||
scrobbleretrieverWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newScrobbleRetrieverGetFirstTimestampHostFunction(service ScrobbleRetrieverService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"scrobbleretriever_getfirsttimestamp",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read JSON request from plugin memory
|
||||
reqBytes, err := p.ReadBytes(stack[0])
|
||||
if err != nil {
|
||||
scrobbleretrieverWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
var req ScrobbleRetrieverGetFirstTimestampRequest
|
||||
if err := json.Unmarshal(reqBytes, &req); err != nil {
|
||||
scrobbleretrieverWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
result, svcErr := service.GetFirstTimestamp(ctx, req.Username)
|
||||
if svcErr != nil {
|
||||
scrobbleretrieverWriteError(p, stack, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Write JSON response to plugin memory
|
||||
resp := ScrobbleRetrieverGetFirstTimestampResponse{
|
||||
Result: result,
|
||||
}
|
||||
scrobbleretrieverWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newScrobbleRetrieverGetScrobblesHostFunction(service ScrobbleRetrieverService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"scrobbleretriever_getscrobbles",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read JSON request from plugin memory
|
||||
reqBytes, err := p.ReadBytes(stack[0])
|
||||
if err != nil {
|
||||
scrobbleretrieverWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
var req ScrobbleRetrieverGetScrobblesRequest
|
||||
if err := json.Unmarshal(reqBytes, &req); err != nil {
|
||||
scrobbleretrieverWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
result, svcErr := service.GetScrobbles(ctx, req.Username, req.Options)
|
||||
if svcErr != nil {
|
||||
scrobbleretrieverWriteError(p, stack, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Write JSON response to plugin memory
|
||||
resp := ScrobbleRetrieverGetScrobblesResponse{
|
||||
Result: result,
|
||||
}
|
||||
scrobbleretrieverWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
// scrobbleretrieverWriteResponse writes a JSON response to plugin memory.
|
||||
func scrobbleretrieverWriteResponse(p *extism.CurrentPlugin, stack []uint64, resp any) {
|
||||
respBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
scrobbleretrieverWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
respPtr, err := p.WriteBytes(respBytes)
|
||||
if err != nil {
|
||||
stack[0] = 0
|
||||
return
|
||||
}
|
||||
stack[0] = respPtr
|
||||
}
|
||||
|
||||
// scrobbleretrieverWriteError writes an error response to plugin memory.
|
||||
func scrobbleretrieverWriteError(p *extism.CurrentPlugin, stack []uint64, err error) {
|
||||
errResp := struct {
|
||||
Error string `json:"error"`
|
||||
}{Error: err.Error()}
|
||||
respBytes, _ := json.Marshal(errResp)
|
||||
respPtr, _ := p.WriteBytes(respBytes)
|
||||
stack[0] = respPtr
|
||||
}
|
||||
122
plugins/host_scrobbleretriever.go
Normal file
122
plugins/host_scrobbleretriever.go
Normal file
@ -0,0 +1,122 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/plugins/host"
|
||||
)
|
||||
|
||||
type scrobbleRetrieverServiceImpl struct {
|
||||
ds model.DataStore
|
||||
users userAccess
|
||||
}
|
||||
|
||||
func newScrobbleRetreverService(ds model.DataStore, users userAccess) host.ScrobbleRetrieverService {
|
||||
return &scrobbleRetrieverServiceImpl{
|
||||
ds: ds,
|
||||
users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *scrobbleRetrieverServiceImpl) getUserContext(ctx context.Context, username string) (context.Context, error) {
|
||||
usr, err := s.users.resolve(ctx, s.ds, username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scrobbleRetriever: %w", err)
|
||||
}
|
||||
|
||||
ctx = request.WithUser(ctx, *usr)
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (s *scrobbleRetrieverServiceImpl) getFirstLastScrobble(ctx context.Context, username string, order string) (*int64, error) {
|
||||
ctx, err := s.getUserContext(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scrobbles, err := s.ds.Scrobble(ctx).GetAll(model.QueryOptions{Sort: "submission_time", Order: order, Max: 1})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(scrobbles) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &scrobbles[0].SubmissionTime, nil
|
||||
}
|
||||
|
||||
func (s *scrobbleRetrieverServiceImpl) GetFirstTimestamp(ctx context.Context, username string) (*int64, error) {
|
||||
return s.getFirstLastScrobble(ctx, username, "ASC")
|
||||
}
|
||||
|
||||
func (s *scrobbleRetrieverServiceImpl) GetLastTimestamp(ctx context.Context, username string) (*int64, error) {
|
||||
return s.getFirstLastScrobble(ctx, username, "DESC")
|
||||
}
|
||||
|
||||
func (s *scrobbleRetrieverServiceImpl) GetScrobbles(ctx context.Context, username string, options host.ScrobbleOptions) (*host.ScrobbleList, error) {
|
||||
ctx, err := s.getUserContext(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if options.MaxItems == 0 {
|
||||
options.MaxItems = 5000
|
||||
}
|
||||
|
||||
// Fetch one more item than requested. The last item is the next timestamp to fetch
|
||||
options.MaxItems += 1
|
||||
|
||||
var filters squirrel.Sqlizer
|
||||
if options.FromTimestamp != nil {
|
||||
filters = squirrel.GtOrEq{"submission_time": *options.FromTimestamp}
|
||||
}
|
||||
|
||||
if options.ToTimestamp != nil {
|
||||
filters = squirrel.And{filters, squirrel.LtOrEq{"submission_time": *options.ToTimestamp}}
|
||||
}
|
||||
|
||||
var order string
|
||||
if options.ToTimestamp != nil && options.FromTimestamp == nil {
|
||||
order = "DESC"
|
||||
} else {
|
||||
order = "ASC"
|
||||
}
|
||||
|
||||
scrobbles, err := s.ds.Scrobble(ctx).GetAll(model.QueryOptions{
|
||||
Max: options.MaxItems,
|
||||
Filters: filters,
|
||||
Sort: "submission_time",
|
||||
Order: order,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var nextTimestamp *int64
|
||||
|
||||
if len(scrobbles) == options.MaxItems {
|
||||
nextTimestamp = &scrobbles[options.MaxItems-1].SubmissionTime
|
||||
}
|
||||
|
||||
scrobbleRefs := make([]host.ScrobbleRef, options.MaxItems-1)
|
||||
for idx := range scrobbleRefs {
|
||||
scrobbleRefs[idx].ID = scrobbles[idx].ID
|
||||
scrobbleRefs[idx].MediaFileID = scrobbles[idx].MediaFileID
|
||||
scrobbleRefs[idx].SubmissionTime = scrobbles[idx].SubmissionTime
|
||||
}
|
||||
|
||||
response := host.ScrobbleList{
|
||||
Scrobbles: scrobbleRefs,
|
||||
NextTimestamp: nextTimestamp,
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
var _ host.ScrobbleRetrieverService = (*scrobbleRetrieverServiceImpl)(nil)
|
||||
@ -168,6 +168,14 @@ var hostServices = []hostServiceEntry{
|
||||
return host.RegisterTaskHostFunctions(service), service, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ScrobbleRetriever",
|
||||
hasPermission: func(p *Permissions) bool { return p != nil && p.ScrobbleRetriever != nil },
|
||||
create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer, error) {
|
||||
service := newScrobbleRetreverService(ctx.manager.ds, newUserAccess(ctx.allowedUsers, ctx.allUsers))
|
||||
return host.RegisterScrobbleRetrieverHostFunctions(service), nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// extractManifest reads manifest from an .ndp package and computes its SHA-256 hash.
|
||||
|
||||
@ -116,6 +116,9 @@
|
||||
},
|
||||
"matcher": {
|
||||
"$ref": "#/$defs/MatcherPermission"
|
||||
},
|
||||
"scrobbleRetriever": {
|
||||
"$ref": "#/$defs/ScrobbleRetrieverPermission"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -268,6 +271,17 @@
|
||||
"description": "Explanation for why matcher access is needed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ScrobbleRetrieverPermission": {
|
||||
"type": "object",
|
||||
"description": "Scrobble retriever permissions for retrieving scrobbles from users",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Explanation for why scrobble retriever access is needed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +187,9 @@ type Permissions struct {
|
||||
// Scheduler corresponds to the JSON schema field "scheduler".
|
||||
Scheduler *SchedulerPermission `json:"scheduler,omitempty" yaml:"scheduler,omitempty" mapstructure:"scheduler,omitempty"`
|
||||
|
||||
// ScrobbleRetriever corresponds to the JSON schema field "scrobbleRetriever".
|
||||
ScrobbleRetriever *ScrobbleRetrieverPermission `json:"scrobbleRetriever,omitempty" yaml:"scrobbleRetriever,omitempty" mapstructure:"scrobbleRetriever,omitempty"`
|
||||
|
||||
// Subsonicapi corresponds to the JSON schema field "subsonicapi".
|
||||
Subsonicapi *SubsonicAPIPermission `json:"subsonicapi,omitempty" yaml:"subsonicapi,omitempty" mapstructure:"subsonicapi,omitempty"`
|
||||
|
||||
@ -206,6 +209,12 @@ type SchedulerPermission struct {
|
||||
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// Scrobble retriever permissions for retrieving scrobbles from users
|
||||
type ScrobbleRetrieverPermission struct {
|
||||
// Explanation for why scrobble retriever access is needed
|
||||
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// SubsonicAPI service permissions. Requires 'users' permission to be declared.
|
||||
type SubsonicAPIPermission struct {
|
||||
// Explanation for why SubsonicAPI access is needed
|
||||
|
||||
@ -43,6 +43,7 @@ The following host services are available:
|
||||
- Library: provides access to music library metadata for plugins.
|
||||
- Matcher: resolves externally-obtained songs to local library tracks,
|
||||
- Scheduler: provides task scheduling capabilities for plugins.
|
||||
- ScrobbleRetriever: allows a plugin to retrieve scrobbles for one or more authorized users.
|
||||
- SubsonicAPI: provides access to Navidrome's Subsonic API from plugins.
|
||||
- Task: provides persistent task queues for plugins.
|
||||
- Users: provides access to user information for plugins.
|
||||
|
||||
183
plugins/pdk/go/host/nd_host_scrobbleretriever.go
Normal file
183
plugins/pdk/go/host/nd_host_scrobbleretriever.go
Normal file
@ -0,0 +1,183 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains client wrappers for the ScrobbleRetriever host service.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
|
||||
)
|
||||
|
||||
// ScrobbleList represents the ScrobbleList data structure.
|
||||
type ScrobbleList struct {
|
||||
Scrobbles []ScrobbleRef `json:"scrobbles"`
|
||||
NextTimestamp *int64 `json:"nextTimestamp"`
|
||||
}
|
||||
|
||||
// ScrobbleOptions represents the ScrobbleOptions data structure.
|
||||
type ScrobbleOptions struct {
|
||||
FromTimestamp *int64 `json:"fromTimestamp"`
|
||||
ToTimestamp *int64 `json:"toTimestamp"`
|
||||
MaxItems int `json:"maxItems"`
|
||||
}
|
||||
|
||||
// ScrobbleRef represents the ScrobbleRef data structure.
|
||||
type ScrobbleRef struct {
|
||||
ID int64 `json:"id"`
|
||||
MediaFileID string `json:"mediaFileId"`
|
||||
SubmissionTime int64 `json:"submissionTime"`
|
||||
}
|
||||
|
||||
// scrobbleretriever_getlasttimestamp is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user scrobbleretriever_getlasttimestamp
|
||||
func scrobbleretriever_getlasttimestamp(uint64) uint64
|
||||
|
||||
// scrobbleretriever_getfirsttimestamp is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user scrobbleretriever_getfirsttimestamp
|
||||
func scrobbleretriever_getfirsttimestamp(uint64) uint64
|
||||
|
||||
// scrobbleretriever_getscrobbles is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user scrobbleretriever_getscrobbles
|
||||
func scrobbleretriever_getscrobbles(uint64) uint64
|
||||
|
||||
type scrobbleRetrieverGetLastTimestampRequest struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type scrobbleRetrieverGetLastTimestampResponse struct {
|
||||
Result *int64 `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type scrobbleRetrieverGetFirstTimestampRequest struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type scrobbleRetrieverGetFirstTimestampResponse struct {
|
||||
Result *int64 `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type scrobbleRetrieverGetScrobblesRequest struct {
|
||||
Username string `json:"username"`
|
||||
Options ScrobbleOptions `json:"options"`
|
||||
}
|
||||
|
||||
type scrobbleRetrieverGetScrobblesResponse struct {
|
||||
Result *ScrobbleList `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetLastTimestamp calls the scrobbleretriever_getlasttimestamp host function.
|
||||
// GetLastTimestamp returns the unix timestamp of the most recent scrobble for the user
|
||||
func ScrobbleRetrieverGetLastTimestamp(username string) (*int64, error) {
|
||||
// Marshal request to JSON
|
||||
req := scrobbleRetrieverGetLastTimestampRequest{
|
||||
Username: username,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqMem := pdk.AllocateBytes(reqBytes)
|
||||
defer reqMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := scrobbleretriever_getlasttimestamp(reqMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response scrobbleRetrieverGetLastTimestampResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert Error field to Go error
|
||||
if response.Error != "" {
|
||||
return nil, errors.New(response.Error)
|
||||
}
|
||||
|
||||
return response.Result, nil
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetFirstTimestamp calls the scrobbleretriever_getfirsttimestamp host function.
|
||||
// GetFirstTimestamp returns the unix timestamp of the oldest scrobble for the user
|
||||
func ScrobbleRetrieverGetFirstTimestamp(username string) (*int64, error) {
|
||||
// Marshal request to JSON
|
||||
req := scrobbleRetrieverGetFirstTimestampRequest{
|
||||
Username: username,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqMem := pdk.AllocateBytes(reqBytes)
|
||||
defer reqMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := scrobbleretriever_getfirsttimestamp(reqMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response scrobbleRetrieverGetFirstTimestampResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert Error field to Go error
|
||||
if response.Error != "" {
|
||||
return nil, errors.New(response.Error)
|
||||
}
|
||||
|
||||
return response.Result, nil
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetScrobbles calls the scrobbleretriever_getscrobbles host function.
|
||||
func ScrobbleRetrieverGetScrobbles(username string, options ScrobbleOptions) (*ScrobbleList, error) {
|
||||
// Marshal request to JSON
|
||||
req := scrobbleRetrieverGetScrobblesRequest{
|
||||
Username: username,
|
||||
Options: options,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqMem := pdk.AllocateBytes(reqBytes)
|
||||
defer reqMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := scrobbleretriever_getscrobbles(reqMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response scrobbleRetrieverGetScrobblesResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert Error field to Go error
|
||||
if response.Error != "" {
|
||||
return nil, errors.New(response.Error)
|
||||
}
|
||||
|
||||
return response.Result, nil
|
||||
}
|
||||
77
plugins/pdk/go/host/nd_host_scrobbleretriever_stub.go
Normal file
77
plugins/pdk/go/host/nd_host_scrobbleretriever_stub.go
Normal file
@ -0,0 +1,77 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains mock implementations for non-WASM builds.
|
||||
// These mocks allow IDE support, compilation, and unit testing on non-WASM platforms.
|
||||
// Plugin authors can use the exported mock instances to set expectations in tests.
|
||||
//
|
||||
//go:build !wasip1
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ScrobbleList represents the ScrobbleList data structure.
|
||||
type ScrobbleList struct {
|
||||
Scrobbles []ScrobbleRef `json:"scrobbles"`
|
||||
NextTimestamp *int64 `json:"nextTimestamp"`
|
||||
}
|
||||
|
||||
// ScrobbleOptions represents the ScrobbleOptions data structure.
|
||||
type ScrobbleOptions struct {
|
||||
FromTimestamp *int64 `json:"fromTimestamp"`
|
||||
ToTimestamp *int64 `json:"toTimestamp"`
|
||||
MaxItems int `json:"maxItems"`
|
||||
}
|
||||
|
||||
// ScrobbleRef represents the ScrobbleRef data structure.
|
||||
type ScrobbleRef struct {
|
||||
ID int64 `json:"id"`
|
||||
MediaFileID string `json:"mediaFileId"`
|
||||
SubmissionTime int64 `json:"submissionTime"`
|
||||
}
|
||||
|
||||
// mockScrobbleRetrieverService is the mock implementation for testing.
|
||||
type mockScrobbleRetrieverService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverMock is the auto-instantiated mock instance for testing.
|
||||
// Use this to set expectations: host.ScrobbleRetrieverMock.On("MethodName", args...).Return(values...)
|
||||
var ScrobbleRetrieverMock = &mockScrobbleRetrieverService{}
|
||||
|
||||
// GetLastTimestamp is the mock method for ScrobbleRetrieverGetLastTimestamp.
|
||||
func (m *mockScrobbleRetrieverService) GetLastTimestamp(username string) (*int64, error) {
|
||||
args := m.Called(username)
|
||||
return args.Get(0).(*int64), args.Error(1)
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetLastTimestamp delegates to the mock instance.
|
||||
// GetLastTimestamp returns the unix timestamp of the most recent scrobble for the user
|
||||
func ScrobbleRetrieverGetLastTimestamp(username string) (*int64, error) {
|
||||
return ScrobbleRetrieverMock.GetLastTimestamp(username)
|
||||
}
|
||||
|
||||
// GetFirstTimestamp is the mock method for ScrobbleRetrieverGetFirstTimestamp.
|
||||
func (m *mockScrobbleRetrieverService) GetFirstTimestamp(username string) (*int64, error) {
|
||||
args := m.Called(username)
|
||||
return args.Get(0).(*int64), args.Error(1)
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetFirstTimestamp delegates to the mock instance.
|
||||
// GetFirstTimestamp returns the unix timestamp of the oldest scrobble for the user
|
||||
func ScrobbleRetrieverGetFirstTimestamp(username string) (*int64, error) {
|
||||
return ScrobbleRetrieverMock.GetFirstTimestamp(username)
|
||||
}
|
||||
|
||||
// GetScrobbles is the mock method for ScrobbleRetrieverGetScrobbles.
|
||||
func (m *mockScrobbleRetrieverService) GetScrobbles(username string, options ScrobbleOptions) (*ScrobbleList, error) {
|
||||
args := m.Called(username, options)
|
||||
return args.Get(0).(*ScrobbleList), args.Error(1)
|
||||
}
|
||||
|
||||
// ScrobbleRetrieverGetScrobbles delegates to the mock instance.
|
||||
func ScrobbleRetrieverGetScrobbles(username string, options ScrobbleOptions) (*ScrobbleList, error) {
|
||||
return ScrobbleRetrieverMock.GetScrobbles(username, options)
|
||||
}
|
||||
@ -40,6 +40,7 @@
|
||||
//! - [`library`] - provides access to music library metadata for plugins.
|
||||
//! - [`matcher`] - resolves externally-obtained songs to local library tracks,
|
||||
//! - [`scheduler`] - provides task scheduling capabilities for plugins.
|
||||
//! - [`scrobbleretriever`] - allows a plugin to retrieve scrobbles for one or more authorized users.
|
||||
//! - [`subsonicapi`] - provides access to Navidrome's Subsonic API from plugins.
|
||||
//! - [`task`] - provides persistent task queues for plugins.
|
||||
//! - [`users`] - provides access to user information for plugins.
|
||||
@ -101,6 +102,13 @@ pub mod scheduler {
|
||||
pub use super::nd_host_scheduler::*;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
mod nd_host_scrobbleretriever;
|
||||
/// allows a plugin to retrieve scrobbles for one or more authorized users.
|
||||
pub mod scrobbleretriever {
|
||||
pub use super::nd_host_scrobbleretriever::*;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
mod nd_host_subsonicapi;
|
||||
/// provides access to Navidrome's Subsonic API from plugins.
|
||||
|
||||
160
plugins/pdk/rust/nd-pdk-host/src/nd_host_scrobbleretriever.rs
Normal file
160
plugins/pdk/rust/nd-pdk-host/src/nd_host_scrobbleretriever.rs
Normal file
@ -0,0 +1,160 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains client wrappers for the ScrobbleRetriever host service.
|
||||
// It is intended for use in Navidrome plugins built with extism-pdk.
|
||||
|
||||
use extism_pdk::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScrobbleList {
|
||||
pub scrobbles: Vec<ScrobbleRef>,
|
||||
#[serde(default)]
|
||||
pub next_timestamp: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScrobbleOptions {
|
||||
#[serde(default)]
|
||||
pub from_timestamp: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub to_timestamp: Option<i64>,
|
||||
pub max_items: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScrobbleRef {
|
||||
pub id: i64,
|
||||
pub media_file_id: String,
|
||||
pub submission_time: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ScrobbleRetrieverGetLastTimestampRequest {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ScrobbleRetrieverGetLastTimestampResponse {
|
||||
#[serde(default)]
|
||||
result: Option<i64>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ScrobbleRetrieverGetFirstTimestampRequest {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ScrobbleRetrieverGetFirstTimestampResponse {
|
||||
#[serde(default)]
|
||||
result: Option<i64>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ScrobbleRetrieverGetScrobblesRequest {
|
||||
username: String,
|
||||
options: ScrobbleOptions,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ScrobbleRetrieverGetScrobblesResponse {
|
||||
#[serde(default)]
|
||||
result: Option<ScrobbleList>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[host_fn]
|
||||
extern "ExtismHost" {
|
||||
fn scrobbleretriever_getlasttimestamp(input: Json<ScrobbleRetrieverGetLastTimestampRequest>) -> Json<ScrobbleRetrieverGetLastTimestampResponse>;
|
||||
fn scrobbleretriever_getfirsttimestamp(input: Json<ScrobbleRetrieverGetFirstTimestampRequest>) -> Json<ScrobbleRetrieverGetFirstTimestampResponse>;
|
||||
fn scrobbleretriever_getscrobbles(input: Json<ScrobbleRetrieverGetScrobblesRequest>) -> Json<ScrobbleRetrieverGetScrobblesResponse>;
|
||||
}
|
||||
|
||||
/// GetLastTimestamp returns the unix timestamp of the most recent scrobble for the user
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `username` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// The result value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_last_timestamp(username: &str) -> Result<Option<i64>, Error> {
|
||||
let response = unsafe {
|
||||
scrobbleretriever_getlasttimestamp(Json(ScrobbleRetrieverGetLastTimestampRequest {
|
||||
username: username.to_owned(),
|
||||
}))?
|
||||
};
|
||||
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok(response.0.result)
|
||||
}
|
||||
|
||||
/// GetFirstTimestamp returns the unix timestamp of the oldest scrobble for the user
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `username` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// The result value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_first_timestamp(username: &str) -> Result<Option<i64>, Error> {
|
||||
let response = unsafe {
|
||||
scrobbleretriever_getfirsttimestamp(Json(ScrobbleRetrieverGetFirstTimestampRequest {
|
||||
username: username.to_owned(),
|
||||
}))?
|
||||
};
|
||||
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok(response.0.result)
|
||||
}
|
||||
|
||||
/// Calls the scrobbleretriever_getscrobbles host function.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `username` - String parameter.
|
||||
/// * `options` - ScrobbleOptions parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// The result value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_scrobbles(username: &str, options: ScrobbleOptions) -> Result<Option<ScrobbleList>, Error> {
|
||||
let response = unsafe {
|
||||
scrobbleretriever_getscrobbles(Json(ScrobbleRetrieverGetScrobblesRequest {
|
||||
username: username.to_owned(),
|
||||
options: options,
|
||||
}))?
|
||||
};
|
||||
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok(response.0.result)
|
||||
}
|
||||
16
plugins/testdata/test-scrobble-retriever/go.mod
vendored
Normal file
16
plugins/testdata/test-scrobble-retriever/go.mod
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
module test-sonic-similarity
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/navidrome/navidrome/plugins/pdk/go v0.0.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/extism/go-pdk v1.1.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/navidrome/navidrome/plugins/pdk/go => ../../pdk/go
|
||||
14
plugins/testdata/test-scrobble-retriever/go.sum
vendored
Normal file
14
plugins/testdata/test-scrobble-retriever/go.sum
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
|
||||
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
42
plugins/testdata/test-scrobble-retriever/main.go
vendored
Normal file
42
plugins/testdata/test-scrobble-retriever/main.go
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/host"
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
||||
|
||||
type TestScrobbleTimestampOutput struct {
|
||||
Timestamp *int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
//go:wasmexport call_get_first_timestamp
|
||||
func callGetFirstTimestamp() int32 {
|
||||
username := pdk.InputString()
|
||||
|
||||
time, err := host.ScrobbleRetrieverGetFirstTimestamp(username)
|
||||
if err != nil {
|
||||
pdk.SetErrorString("failed to call scrobble retriever api " + err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
pdk.OutputJSON(TestScrobbleTimestampOutput{Timestamp: time})
|
||||
return 0
|
||||
}
|
||||
|
||||
//go:wasmexport call_get_last_timestamp
|
||||
func callGetLastTimestamp() int32 {
|
||||
username := pdk.InputString()
|
||||
|
||||
time, err := host.ScrobbleRetrieverGetLastTimestamp(username)
|
||||
if err != nil {
|
||||
pdk.SetErrorString("failed to call scrobble retriever api " + err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
pdk.OutputJSON(TestScrobbleTimestampOutput{Timestamp: time})
|
||||
return 0
|
||||
}
|
||||
14
plugins/testdata/test-scrobble-retriever/manifest.json
vendored
Normal file
14
plugins/testdata/test-scrobble-retriever/manifest.json
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Test Scrobble Retriever",
|
||||
"author": "Navidrome Test",
|
||||
"version": "1.0.0",
|
||||
"description": "A test plugin for scrobble retriever integration settings",
|
||||
"permissions": {
|
||||
"scrobbleRetriever": {
|
||||
"reason": "For testing scrobble retriever operations"
|
||||
},
|
||||
"users": {
|
||||
"reason": "Access user information for scrobble retrieval"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user