navidrome/plugins/manifest_gen.go
Kendall Garner b0c6d2e444
feat(plugins): add scrobbles access to PDK (#5795)
* initial scrobble api

* feat: add scrobble retrieval api

* address feedback (1)

* fix spelling

* be explicit about get

* add primary key field, update index, remove rowid references

* use unix timestamp for input and output

* initial api, some testing

* add tests, add count retrieval

* add docs, test for rejected user

* add permission validation for scrobble retriever

* chore(plugins): fix typos in scrobble retriever

Rename newScrobbleRetreverService, and fix FromTImestamp/nonero in the
ScrobbleRetriever doc comments, which generate into the Go and Rust PDKs.
Also corrects two mislabelled test entries.

* fix(plugins): make scrobble pagination order deterministic

Sorting only by submission_time left the order of equal timestamps up to the
query planner, but the cursor skips ties by offset, so an unstable order can
repeat or drop scrobbles between pages. Break ties on scrobbles.id, which the
existing scrobbles_user_time index already yields for free.

Descending is now honoured for every combination of From/To rather than only
when both or neither is set. This changes the default for a lone ToTimestamp
from newest-first to oldest-first.

* refactor(plugins): return the next page's options from GetScrobbles

Paging previously meant reading NextTimestamp and Cursor off the response and
deciding where each belonged: NextTimestamp into FromTimestamp when ascending
or ToTimestamp when descending, and Cursor copied every time, including when 0.
Both are silent data-loss bugs when a plugin gets them wrong.

GetScrobbles now returns the options for the following page, or nil when the
range is exhausted, so a plugin passes the value straight back and repeats.
ScrobbleCursor and ScrobbleList are gone; the query itself is unchanged.

* docs(plugins): warn against setting ScrobbleOptions.Offset manually

The all-ties carry rule assumes Offset counts already-returned rows at the
boundary timestamp, which only holds for the options GetScrobbles returns.
A hand-built From+Offset combination can silently skip scrobbles, so document
the field as managed pagination state instead of a generic skip.

* docs(plugins): document the ScrobbleRetriever host service in the README

Covers the manifest permissions (including the users requirement), the four
host functions, the options/ref field tables, and the pagination loop with
its two gotchas: the host-managed offset and the adjusted range on the
returned next options.

* chore(plugins): regenerate scrobble retriever stub with nil-safe accessors

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-08-08 22:13:29 -04:00

270 lines
11 KiB
Go

// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT.
package plugins
import "encoding/json"
import "fmt"
// Artwork service permissions for generating artwork URLs
type ArtworkPermission struct {
// Explanation for why artwork access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
}
// Cache service permissions for storing and retrieving data
type CachePermission struct {
// Explanation for why cache access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
}
// Configuration schema for the plugin using JSON Schema (draft-07) and optional
// JSONForms UI Schema
type ConfigDefinition struct {
// JSON Schema (draft-07) defining the plugin's configuration options
Schema map[string]interface{} `json:"schema" yaml:"schema" mapstructure:"schema"`
// Optional JSONForms UI Schema for customizing form layout
UiSchema map[string]interface{} `json:"uiSchema,omitempty" yaml:"uiSchema,omitempty" mapstructure:"uiSchema,omitempty"`
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *ConfigDefinition) UnmarshalJSON(value []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(value, &raw); err != nil {
return err
}
if _, ok := raw["schema"]; raw != nil && !ok {
return fmt.Errorf("field schema in ConfigDefinition: required")
}
type Plain ConfigDefinition
var plain Plain
if err := json.Unmarshal(value, &plain); err != nil {
return err
}
*j = ConfigDefinition(plain)
return nil
}
// HTTP access permissions for a plugin
type HTTPPermission struct {
// Explanation for why HTTP access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
// List of required host patterns for HTTP requests (e.g., 'api.example.com',
// '*.musicbrainz.org')
RequiredHosts []string `json:"requiredHosts,omitempty" yaml:"requiredHosts,omitempty" mapstructure:"requiredHosts,omitempty"`
}
// Key-value store permissions for persistent plugin storage
type KVStorePermission struct {
// Maximum storage size (e.g., '1MB', '500KB'). Default: 1MB
MaxSize *string `json:"maxSize,omitempty" yaml:"maxSize,omitempty" mapstructure:"maxSize,omitempty"`
// Explanation for why key-value store access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
}
// Library service permissions for accessing library metadata and optionally
// filesystem
type LibraryPermission struct {
// Whether the plugin requires read-only filesystem access to library directories
Filesystem bool `json:"filesystem,omitempty" yaml:"filesystem,omitempty" mapstructure:"filesystem,omitempty"`
// Explanation for why library access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *LibraryPermission) UnmarshalJSON(value []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(value, &raw); err != nil {
return err
}
type Plain LibraryPermission
var plain Plain
if err := json.Unmarshal(value, &plain); err != nil {
return err
}
if v, ok := raw["filesystem"]; !ok || v == nil {
plain.Filesystem = false
}
*j = LibraryPermission(plain)
return nil
}
// Plugin manifest for Navidrome plugins
type Manifest struct {
// The author of the plugin
Author string `json:"author" yaml:"author" mapstructure:"author"`
// Config corresponds to the JSON schema field "config".
Config *ConfigDefinition `json:"config,omitempty" yaml:"config,omitempty" mapstructure:"config,omitempty"`
// A brief description of what the plugin does
Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"`
// The display name of the plugin
Name string `json:"name" yaml:"name" mapstructure:"name"`
// Permissions corresponds to the JSON schema field "permissions".
Permissions *Permissions `json:"permissions,omitempty" yaml:"permissions,omitempty" mapstructure:"permissions,omitempty"`
// The version of the plugin (semver recommended)
Version string `json:"version" yaml:"version" mapstructure:"version"`
// URL to the plugin's website or repository
Website *string `json:"website,omitempty" yaml:"website,omitempty" mapstructure:"website,omitempty"`
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *Manifest) UnmarshalJSON(value []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(value, &raw); err != nil {
return err
}
if _, ok := raw["author"]; raw != nil && !ok {
return fmt.Errorf("field author in Manifest: required")
}
if _, ok := raw["name"]; raw != nil && !ok {
return fmt.Errorf("field name in Manifest: required")
}
if _, ok := raw["version"]; raw != nil && !ok {
return fmt.Errorf("field version in Manifest: required")
}
type Plain Manifest
var plain Plain
if err := json.Unmarshal(value, &plain); err != nil {
return err
}
if len(plain.Author) < 1 {
return fmt.Errorf("field %s length: must be >= %d", "author", 1)
}
if len(plain.Name) < 1 {
return fmt.Errorf("field %s length: must be >= %d", "name", 1)
}
if len(plain.Version) < 1 {
return fmt.Errorf("field %s length: must be >= %d", "version", 1)
}
*j = Manifest(plain)
return nil
}
// Matcher service permissions for resolving external songs to local library tracks
type MatcherPermission struct {
// Explanation for why matcher access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
}
// Permissions required by the plugin
type Permissions struct {
// Artwork corresponds to the JSON schema field "artwork".
Artwork *ArtworkPermission `json:"artwork,omitempty" yaml:"artwork,omitempty" mapstructure:"artwork,omitempty"`
// Cache corresponds to the JSON schema field "cache".
Cache *CachePermission `json:"cache,omitempty" yaml:"cache,omitempty" mapstructure:"cache,omitempty"`
// Http corresponds to the JSON schema field "http".
Http *HTTPPermission `json:"http,omitempty" yaml:"http,omitempty" mapstructure:"http,omitempty"`
// Kvstore corresponds to the JSON schema field "kvstore".
Kvstore *KVStorePermission `json:"kvstore,omitempty" yaml:"kvstore,omitempty" mapstructure:"kvstore,omitempty"`
// Library corresponds to the JSON schema field "library".
Library *LibraryPermission `json:"library,omitempty" yaml:"library,omitempty" mapstructure:"library,omitempty"`
// Matcher corresponds to the JSON schema field "matcher".
Matcher *MatcherPermission `json:"matcher,omitempty" yaml:"matcher,omitempty" mapstructure:"matcher,omitempty"`
// 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"`
// Storage corresponds to the JSON schema field "storage".
Storage *StoragePermission `json:"storage,omitempty" yaml:"storage,omitempty" mapstructure:"storage,omitempty"`
// Subsonicapi corresponds to the JSON schema field "subsonicapi".
Subsonicapi *SubsonicAPIPermission `json:"subsonicapi,omitempty" yaml:"subsonicapi,omitempty" mapstructure:"subsonicapi,omitempty"`
// Taskqueue corresponds to the JSON schema field "taskqueue".
Taskqueue *TaskQueuePermission `json:"taskqueue,omitempty" yaml:"taskqueue,omitempty" mapstructure:"taskqueue,omitempty"`
// Users corresponds to the JSON schema field "users".
Users *UsersPermission `json:"users,omitempty" yaml:"users,omitempty" mapstructure:"users,omitempty"`
// Websocket corresponds to the JSON schema field "websocket".
Websocket *WebSocketPermission `json:"websocket,omitempty" yaml:"websocket,omitempty" mapstructure:"websocket,omitempty"`
}
// Scheduler service permissions for scheduling tasks
type SchedulerPermission struct {
// Explanation for why scheduler access is needed
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"`
}
// Storage permissions for enabling persistent read-write storage exclusively for
// the plugin
type StoragePermission struct {
// Explanation for why storage 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
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
}
// Task queue permissions for background task processing
type TaskQueuePermission struct {
// Maximum total concurrent workers across all queues. Default: 1
MaxConcurrency int `json:"maxConcurrency,omitempty" yaml:"maxConcurrency,omitempty" mapstructure:"maxConcurrency,omitempty"`
// Explanation for why task queue access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *TaskQueuePermission) UnmarshalJSON(value []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(value, &raw); err != nil {
return err
}
type Plain TaskQueuePermission
var plain Plain
if err := json.Unmarshal(value, &plain); err != nil {
return err
}
if v, ok := raw["maxConcurrency"]; !ok || v == nil {
plain.MaxConcurrency = 1.0
}
if 1 > plain.MaxConcurrency {
return fmt.Errorf("field %s: must be >= %v", "maxConcurrency", 1)
}
*j = TaskQueuePermission(plain)
return nil
}
// Users service permissions for accessing user information
type UsersPermission struct {
// Explanation for why users access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
}
// WebSocket service permissions for establishing WebSocket connections
type WebSocketPermission struct {
// Explanation for why WebSocket access is needed
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
// List of required host patterns for WebSocket connections (e.g.,
// 'api.example.com', '*.musicbrainz.org')
RequiredHosts []string `json:"requiredHosts,omitempty" yaml:"requiredHosts,omitempty" mapstructure:"requiredHosts,omitempty"`
}