mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
* 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>
270 lines
11 KiB
Go
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"`
|
|
}
|