navidrome/plugins/manifest-schema.json
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

278 lines
8.3 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://navidrome.org/schemas/Manifest.json",
"title": "Manifest",
"description": "Plugin manifest for Navidrome plugins",
"type": "object",
"additionalProperties": false,
"required": ["name", "author", "version"],
"properties": {
"name": {
"type": "string",
"description": "The display name of the plugin",
"minLength": 1
},
"author": {
"type": "string",
"description": "The author of the plugin",
"minLength": 1
},
"version": {
"type": "string",
"description": "The version of the plugin (semver recommended)",
"minLength": 1
},
"description": {
"type": "string",
"description": "A brief description of what the plugin does"
},
"website": {
"type": "string",
"description": "URL to the plugin's website or repository",
"format": "uri"
},
"permissions": {
"$ref": "#/$defs/Permissions"
},
"config": {
"$ref": "#/$defs/ConfigDefinition"
}
},
"$defs": {
"ConfigDefinition": {
"type": "object",
"description": "Configuration schema for the plugin using JSON Schema (draft-07) and optional JSONForms UI Schema",
"additionalProperties": false,
"required": ["schema"],
"properties": {
"schema": {
"type": "object",
"description": "JSON Schema (draft-07) defining the plugin's configuration options"
},
"uiSchema": {
"type": "object",
"description": "Optional JSONForms UI Schema for customizing form layout"
}
}
},
"Permissions": {
"type": "object",
"description": "Permissions required by the plugin",
"additionalProperties": false,
"properties": {
"http": {
"$ref": "#/$defs/HTTPPermission"
},
"subsonicapi": {
"$ref": "#/$defs/SubsonicAPIPermission"
},
"scheduler": {
"$ref": "#/$defs/SchedulerPermission"
},
"websocket": {
"$ref": "#/$defs/WebSocketPermission"
},
"artwork": {
"$ref": "#/$defs/ArtworkPermission"
},
"cache": {
"$ref": "#/$defs/CachePermission"
},
"library": {
"$ref": "#/$defs/LibraryPermission"
},
"kvstore": {
"$ref": "#/$defs/KVStorePermission"
},
"users": {
"$ref": "#/$defs/UsersPermission"
},
"taskqueue": {
"$ref": "#/$defs/TaskQueuePermission"
},
"matcher": {
"$ref": "#/$defs/MatcherPermission"
},
"storage": {
"$ref": "#/$defs/StoragePermission"
},
"scrobbleRetriever": {
"$ref": "#/$defs/ScrobbleRetrieverPermission"
}
}
},
"ArtworkPermission": {
"type": "object",
"description": "Artwork service permissions for generating artwork URLs",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why artwork access is needed"
}
}
},
"CachePermission": {
"type": "object",
"description": "Cache service permissions for storing and retrieving data",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why cache access is needed"
}
}
},
"HTTPPermission": {
"type": "object",
"description": "HTTP access permissions for a plugin",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why HTTP access is needed"
},
"requiredHosts": {
"type": "array",
"description": "List of required host patterns for HTTP requests (e.g., 'api.example.com', '*.musicbrainz.org')",
"items": {
"type": "string"
}
}
}
},
"SubsonicAPIPermission": {
"type": "object",
"description": "SubsonicAPI service permissions. Requires 'users' permission to be declared.",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why SubsonicAPI access is needed"
}
}
},
"SchedulerPermission": {
"type": "object",
"description": "Scheduler service permissions for scheduling tasks",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why scheduler access is needed"
}
}
},
"WebSocketPermission": {
"type": "object",
"description": "WebSocket service permissions for establishing WebSocket connections",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why WebSocket access is needed"
},
"requiredHosts": {
"type": "array",
"description": "List of required host patterns for WebSocket connections (e.g., 'api.example.com', '*.musicbrainz.org')",
"items": {
"type": "string"
}
}
}
},
"LibraryPermission": {
"type": "object",
"description": "Library service permissions for accessing library metadata and optionally filesystem",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why library access is needed"
},
"filesystem": {
"type": "boolean",
"description": "Whether the plugin requires read-only filesystem access to library directories",
"default": false
}
}
},
"KVStorePermission": {
"type": "object",
"description": "Key-value store permissions for persistent plugin storage",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why key-value store access is needed"
},
"maxSize": {
"type": "string",
"description": "Maximum storage size (e.g., '1MB', '500KB'). Default: 1MB"
}
}
},
"TaskQueuePermission": {
"type": "object",
"description": "Task queue permissions for background task processing",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why task queue access is needed"
},
"maxConcurrency": {
"type": "integer",
"description": "Maximum total concurrent workers across all queues. Default: 1",
"minimum": 1,
"default": 1
}
}
},
"UsersPermission": {
"type": "object",
"description": "Users service permissions for accessing user information",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why users access is needed"
}
}
},
"MatcherPermission": {
"type": "object",
"description": "Matcher service permissions for resolving external songs to local library tracks",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why matcher access is needed"
}
}
},
"StoragePermission": {
"type": "object",
"description": "Storage permissions for enabling persistent read-write storage exclusively for the plugin",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string",
"description": "Explanation for why storage 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"
}
}
}
}
}