navidrome/plugins/capabilities/lyrics.yaml

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

140 lines
5.8 KiB
YAML
Raw Permalink Normal View History

version: v1-draft
exports:
nd_lyrics_get_lyrics:
input:
$ref: '#/components/schemas/GetLyricsRequest'
contentType: application/json
output:
$ref: '#/components/schemas/GetLyricsResponse'
contentType: application/json
components:
schemas:
GetLyricsRequest:
description: GetLyricsRequest contains the track information for lyrics lookup.
properties:
track:
$ref: '#/components/schemas/TrackInfo'
required:
- track
GetLyricsResponse:
description: GetLyricsResponse contains the lyrics returned by the plugin.
properties:
lyrics:
type: array
items:
$ref: '#/components/schemas/LyricsText'
required:
- lyrics
LyricsText:
description: |-
LyricsText represents a single set of lyrics in raw text format.
Text can be plain text or LRC format — Navidrome will parse it.
properties:
lang:
type: string
text:
type: string
required:
- text
TrackInfo:
description: TrackInfo contains track metadata.
properties:
id:
type: string
description: ID is the internal Navidrome track ID.
title:
type: string
description: Title is the track title.
album:
type: string
description: Album is the album name.
artist:
type: string
description: Artist is the formatted artist name for display (e.g., "Artist1 • Artist2").
albumArtist:
type: string
description: AlbumArtist is the formatted album artist name for display.
artists:
type: array
description: Artists is the list of track artists.
items:
$ref: '#/components/schemas/ArtistRef'
albumArtists:
type: array
description: AlbumArtists is the list of album artists.
items:
$ref: '#/components/schemas/ArtistRef'
duration:
type: number
format: float
description: Duration is the track duration in seconds.
trackNumber:
type: integer
format: int32
description: TrackNumber is the track number on the album.
discNumber:
type: integer
format: int32
description: DiscNumber is the disc number.
mbzRecordingId:
type: string
description: MBZRecordingID is the MusicBrainz recording ID.
mbzAlbumId:
type: string
description: MBZAlbumID is the MusicBrainz album/release ID.
mbzReleaseGroupId:
type: string
description: MBZReleaseGroupID is the MusicBrainz release group ID.
mbzReleaseTrackId:
type: string
description: MBZReleaseTrackID is the MusicBrainz release track ID.
feat(subsonic): add sonicSimilarity extension as plugin capability (#5419) * feat(plugins): add sonicSimilarity capability types Defines the SonicSimilarity plugin capability interface with GetSonicSimilarTracks and FindSonicPath methods, along with their request/response types. * feat(sonic): add core sonic similarity service Implements the Sonic service with HasProvider, GetSonicSimilarTracks, and FindSonicPath, delegating to the PluginLoader and using the Matcher for index-preserving library resolution. * test(sonic): add sonic service unit tests Covers HasProvider, GetSonicSimilarTracks, and FindSonicPath with mock plugin loader and provider, verifying error propagation and successful match resolution via the library matcher. * feat(matcher): add MatchSongsToLibraryMap for index-preserving matching Adds a new method alongside MatchSongsToLibrary that returns a map[int]MediaFile keyed by input song index rather than a flat slice, enabling callers to correlate similarity scores back to the original position in the results. * fix(sonic): check provider availability before MediaFile lookup Avoids unnecessary DB call when no plugin is available, and ensures the correct error path is tested. * feat(plugins): add sonic similarity adapter Adds SonicSimilarityAdapter implementing sonic.Provider, bridging the plugin system to the core sonic service via Extism plugin functions. Reuses existing songRefsToAgentSongs helper for SongRef conversion. * feat(plugins): add LoadSonicSimilarity to plugin manager Adds Manager.LoadSonicSimilarity method following the pattern of LoadLyricsProvider, enabling the core sonic service to load a SonicSimilarityAdapter from a named plugin. * feat(subsonic): add sonicMatch response type Add SonicMatch struct with Entry and Similarity fields, and a SonicMatches slice to the Subsonic response struct. These types support the OpenSubsonic sonicSimilarity extension for returning similarity-scored track results. * feat(subsonic): add getSonicSimilarTracks and findSonicPath handlers Add two new Subsonic API handlers for the sonicSimilarity OpenSubsonic extension: GetSonicSimilarTracks returns similarity-scored tracks similar to a given song, and FindSonicPath returns a path of tracks connecting two songs. Both handlers delegate to the sonic core service and map results to SonicMatch response types. * feat(subsonic): advertise sonicSimilarity extension when plugin available Update GetOpenSubsonicExtensions to conditionally include the sonicSimilarity extension only when a sonic similarity plugin provider is available. The nil guard ensures backward compatibility with tests that pass nil for the sonic field. Also update the existing test to pass the new nil parameter. * feat(subsonic): wire sonic similarity service into router Add the sonic.Sonic service to the Router struct and New() constructor, register the getSonicSimilarTracks and findSonicPath routes, and wire sonic.New and its PluginLoader binding into the Wire dependency injection graph. Update all existing test call sites to pass the new nil parameter. Regenerate wire_gen.go. * fix(e2e): add sonic parameter to subsonic.New call in e2e tests * test(subsonic): add sonicSimilarity extension advertisement tests Restructures the GetOpenSubsonicExtensions test into two contexts: one verifying the baseline 5 extensions are returned when no sonic similarity plugin is configured, and one verifying that the sonicSimilarity extension is advertised (making 6 total) when a plugin loader reports an available provider. Adds a mockSonicPluginLoader to satisfy the sonic.PluginLoader interface without requiring a real plugin. * feat(subsonic): add nil guard and e2e tests for sonic similarity endpoints Handlers return ErrorDataNotFound when no sonic service is configured, preventing nil panics. E2e tests verify both endpoints return proper error responses when no plugin is available. * fix(subsonic): return HTTP 404 when no sonic similarity plugin available Endpoints are always registered but return 404 when no provider is available, rather than a subsonic error code 70. * refactor: clean up sonic similarity code after review Extract shared helpers to reduce duplication across the sonic similarity implementation: loadAllMatches in matcher consolidates the 4-phase matching pipeline, songRefToAgentSong eliminates per-iteration slice allocation in the adapter, sonicMatchResponse deduplicates response building in handlers, and a package-level constant replaces raw capability name strings in core/sonic. * fix empty response shapes Signed-off-by: Deluan <deluan@navidrome.org> * test(plugins): add testdata plugin and e2e tests for sonic similarity Add a test-sonic-similarity WASM plugin that implements both GetSonicSimilarTracks and FindSonicPath via the generated sonicsimilarity PDK. The plugin returns deterministic test data with decreasing similarity scores and supports error injection via config. Adapter tests verify the full round-trip through the WASM plugin including error handling. Also includes regenerated PDK code from make gen. * docs: update README to include new capabilities and usage examples for plugins Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): enhance sonic similarity tests with additional scenarios and mock provider Signed-off-by: Deluan <deluan@navidrome.org> * fix: address PR review feedback for sonic similarity Fix incorrect field names in README documentation ({from, to} → {startSong, endSong}) and remove unnecessary XML serialization test from e2e suite since OpenSubsonic endpoints only use JSON. * refactor: rename Matcher methods for conciseness Rename MatchSongsToLibrary to MatchSongs and MatchSongsToLibraryMap to MatchSongsIndexed. The Matcher receiver already establishes the "to library" context, making that suffix redundant, and "Indexed" better describes the intent (preserving input ordering) than "Map" which describes the data structure. * refactor: standardize variable naming for media files in sonic path methods Signed-off-by: Deluan <deluan@navidrome.org> * refactor: simplify plugin loading by introducing adapter constructors Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-27 17:50:09 -04:00
libraryId:
type: integer
format: int32
description: |-
LibraryID is the ID of the library the track belongs to.
Only included if the plugin has library permission with filesystem access for the track's library.
path:
type: string
description: |-
Path is the full path to the track file, relative to the library root.
Only included if the plugin has library permission with filesystem access for the track's library.
required:
- id
- title
- album
- artist
- albumArtist
- artists
- albumArtists
- duration
- trackNumber
- discNumber
feat(plugins): share plugin DTOs via a types package (#5655) * refactor(plugins): remove Python PDK generation from ndpgen * feat(plugins): parse Go type aliases distinctly in ndpgen * feat(plugins): resolve shared-type aliases against a registry in ndpgen * fix(plugins): resolve host-service shared aliases package-wide Mirror the capability approach in ParseDirectoryWithShared: do a first pass over all package files to build a package-wide alias map, then pass it into parseServiceFile so that a shared-type alias declared in a sibling file is visible when resolving types in the service interface file. Add a focused test that writes the alias in one file and the hostservice in another, confirming RED before the fix and GREEN after. Also strengthens the existing Task 3 test with an ArtistRef.Target assertion. * feat(plugins): add ndpgen -shared-types mode for the Go types package * feat(plugins): generate the nd-pdk-types Rust crate from -shared-types * feat(plugins): inject types import and emit deprecated aliases in Go output * feat(plugins): emit deprecated Rust aliases to the shared types crate * feat(plugins): inline shared-type shapes into XTP schemas * feat(plugins): add nd-pdk-types crate and wire dependents * feat(plugins): move shared capability types to plugins/types with deprecated aliases * fix(plugins): point Rust deprecated-alias note at the replacement type * fix(plugins): include shared aliases in KnownStructs so Rust fields keep their type Capability.KnownStructs() and Service.KnownStructs() previously only registered names from .Structs. After the shared-types migration, types like ArtistRef/TrackInfo/SongRef live in .SharedAliases instead, so ToRustTypeWithStructs could not find them and fell back to serde_json::Value for every struct field referencing a shared type. Add the shared-alias names to the knownStructs map in both methods. Regenerate the Rust capability files; track/song/artist fields now render as their named types (TrackInfo, SongRef, ArtistRef, etc.). Add a regression test that verifies a struct field whose type is only in SharedAliases renders as the named type and not serde_json::Value. * docs(plugins): remove stale Python references from ndpgen and plugins READMEs ndpgen no longer has a -python flag; remove it from the usage synopsis, flags table, and defaults note in ndpgen/README.md. Delete the "Python Client Library" section that described its output. plugins/README.md referenced plugins/pdk/python/host/ (deleted) as the source for Python host-service stubs. Remove that paragraph; Python plugins still work via the XTP-schema / extism-py path (see examples/*-py). * refactor(plugins): dedupe ndpgen helpers and tidy shared-type codegen * docs(plugins): restore Python as a supported XTP schema target The ndpgen-generated Python PDK was removed, but the XTP YAML schemas are language-neutral and the XTP CLI still generates Python bindings from them (as the extism-py examples demonstrate). Only the ndpgen Python output was dropped, not Python support itself. * test(plugins): use the shared types package in test plugins The test fixtures referenced the now-deprecated capability aliases (sonicsimilarity.SongRef, metadata.ArtistRef/SongRef). Point them at the canonical types package so our own fixtures don't depend on symbols slated for removal. * refactor(plugins): use the shared types package in host adapters Replace deprecated capabilities.TrackInfo, capabilities.ArtistRef, and capabilities.SongRef aliases with the canonical types.TrackInfo, types.ArtistRef, and types.SongRef from plugins/types. * fix(plugins): reference shared types by canonical path in generated Rust Previously the generator emitted `pub field: SongRef` (the local deprecated alias) for struct fields whose type came from SharedAliases. Refactored ToRustTypeWithStructs into a private toRustType that accepts a shared map, and added ToRustTypeWithShared which resolves shared-alias names to their canonical nd_pdk_types::X path before falling through to the knownStructs check. Both rustCapabilityFuncMap and rustFuncMap now build the shared map from SharedAliases and use it for fieldRustType, so the generated capability files reference nd_pdk_types::SongRef / nd_pdk_types::TrackInfo directly. The deprecated pub type aliases remain in place as the external back-compat surface. Deprecation warning count from cargo build drops to 0. * fix(examples): implement missing Scrobbler.playback_report in Rust examples The Scrobbler trait gained a playback_report method but the two Rust example plugins (webhook-rs and discord-rich-presence-rs) were not updated, causing E0046 compile errors. Added the missing fn playback_report to both: webhook-rs logs and returns Ok(()) mirroring its now_playing handler; discord-rich-presence-rs is a no-op since Discord presence does not need playback reports. make all-rust now exits 0. * refactor(plugins): point Rust deprecation notes at the nd_pdk::types umbrella path Plugin authors depend on the nd-pdk umbrella crate, which re-exports nd_pdk_types as 'types', so the migration target they should type is nd_pdk::types::X. The alias target stays nd_pdk_types::X (the real path inside nd-pdk-capabilities). * fix(plugins): error when a shared-type alias can't be resolved against the registry * refactor(plugins): parse each Go source file once in ndpgen * fix(plugins): correct ndpgen review nits (flag name, unused dep, docs) * refactor(plugins): drop the now-unused path param from parseServiceFile * refactor(plugins): use shared types directly, rename TrackInfo to Track Capability interfaces now reference the shared `types` package by qualified name (types.Track, types.SongRef, types.ArtistRef) instead of the package-local deprecated aliases, and the shared TrackInfo type is renamed to Track to match its role as the plugin-facing projection of a library media file. The deprecated bare aliases (scrobbler.TrackInfo, metadata.ArtistRef, sonicsimilarity.SongRef, etc.) are kept as re-exports so existing plugins keep compiling, with a deprecation warning steering them to the canonical types. To support this, ndpgen now resolves qualified types.X references: it collects them during type discovery, maps each used canonical type back to its declared deprecated alias for re-export, emits nd_pdk_types::X paths in Rust, and names the XTP schema components by their canonical type. Regenerated the Go and Rust PDK and the XTP schemas, and added generator tests covering the qualified-ref path. Also adds clarifying doc comments to the shared types. * refactor(plugins): extract shared types selector into a named const Replace the "types." string literal that detects and strips the shared types package selector with a single sharedTypesPrefix constant across the ndpgen generator (parser, types, generator, xtp_schema), giving the package one source of truth for the selector. Also restore the single reused scratch map (cleared each iteration) in the resolveSharedAliases BFS instead of allocating a fresh map per shared-struct field, matching the prior implementation. Pure cleanup from a /simplify pass: regeneration produces byte-for-byte identical Go, Rust, and XTP output. * refactor(plugins): keep TrackInfo in the capability package for now Move the track type back out of the shared plugins/types package: it is again defined inline as TrackInfo in plugins/capabilities/scrobbler.go and referenced directly by the scrobbler and lyrics capabilities, reverting the rename to types.Track. The host helper is renamed back to mediaFileToTrackInfo and now returns capabilities.TrackInfo. SongRef and ArtistRef stay in the shared types package; TrackInfo keeps using types.ArtistRef for its artist lists. This type is expected to be reshaped in upcoming work, so leaving it in the capability package avoids churning the shared types twice. Regenerated the Go and Rust PDK and the XTP schemas accordingly. * fix(plugins): emit the Go types import for direct shared-type refs ndpgen's Capability/Service.ImportsSharedTypes only reported a shared-types dependency when a deprecated re-export alias (type X = types.X) was declared. A struct field referencing the canonical form directly (e.g. types.SongRef) with no such alias produced an empty SharedAliases slice, so the Go templates skipped the import while still emitting fields/signatures using types.SongRef — leaving generated PDK code for new shared DTOs uncompilable unless an otherwise unnecessary alias was added. ImportsSharedTypes now also returns true when any struct field references the types. package by qualified name, via a new structsReferenceSharedTypes helper that reuses collectReferencedTypes (so []types.X and map[...]types.X are covered too). * fix(plugins): preserve base64 encoding for shared byte fields in Rust The Rust shared-types crate template rendered a []byte field as a plain Vec<u8> without the base64_bytes serde override used by the capability/client templates. Go's encoding/json serializes []byte as a base64 string, so a Rust plugin using nd_pdk::types would have serialized an array of numbers instead of the wire format the Go/server side expects. GenerateSharedTypesRust now registers the base64_bytes partial and passes a HasByteFields flag (new anyFieldIsByteSlice helper); types.rs.tmpl emits the base64_bytes module and a #[serde(with = "base64_bytes")] attribute on []byte fields, mirroring the capability template. * fix(plugins): include directly-referenced shared types in XTP schemas buildSchemas registered shared types into the schema components only by iterating cap.SharedAliases, which records deprecated re-export aliases. A capability that referenced a shared DTO solely as types.Foo (no declared alias) therefore never got Foo into the component set, so the self-contained XTP schema rendered the field as a generic object (or emitted a dangling $ref), breaking the direct shared-type use case enabled by -shared. resolveSharedAliases now also returns the resolved shapes of every used shared type (alias or not); these are carried on the new Capability.SharedTypes field and registered by buildSchemas alongside SharedAliases. Validated end-to-end with the xtp CLI: a direct types.Foo reference now produces a proper component plus a $ref, so xtp generates a typed struct instead of an untyped serde_json::Map. * fix(plugins): resolve renamed shared aliases to canonical schema refs When a deprecated alias renames its canonical type (e.g. type TrackInfo = types.Track) and a capability field is typed with the alias name (TrackInfo), buildProperty emitted a $ref to #/components/schemas/TrackInfo. Components are keyed by the canonical name (Track), so no TrackInfo component was emitted, leaving a dangling reference that crashes the xtp code generator. buildSchemas now builds an alias->canonical map; buildProperty (and the slice item path) resolves $ref targets through it, and a used alias name marks its canonical component used so it is emitted. Validated with the xtp CLI: the renamed-alias schema previously crashed xtp and now generates cleanly. * fix(plugins): detect shared types used directly in method signatures ImportsSharedTypes only inspected struct fields, so a capability method using a shared type directly in its signature (e.g. types.SongRef as input/output rather than inside a local struct) was not detected. The generated Go templates still rendered the provider/export signatures with types.SongRef, so the capability package omitted the types import and failed to compile; the same gap applied to service params/returns. ImportsSharedTypes now also scans capability method input/output types and service method params/returns, via a typeReferencesSharedTypes helper that reuses collectReferencedTypes (covering pointer/slice/map wrappers). * fix(plugins): add base64 dependency to the shared Rust types crate When a shared DTO has a []byte field, ndpgen emits the base64_bytes serde helper and use base64::... imports into nd-pdk-types/src/lib.rs, but the crate manifest declared only serde. In that case make gen produced a crate that failed to compile with 'unresolved module base64'. Add base64 = "0.22" (matching nd-pdk-capabilities) so the generated shared types crate compiles whenever a []byte field is present. Verified by generating a shared crate with a []byte field and confirming cargo check fails before and passes after. * fix(plugins): translate shared method types in generated Rust A capability method using a shared DTO directly as input/output (e.g. types.SongRef) was passed through rustOutputType unchanged, so the Rust template emitted invalid trait and extism_pdk::Json<$crate::pkg::types.SongRef> signatures that do not compile. Method input/output types now resolve through the shared registry: trait signatures use rustTraitType (shared -> nd_pdk_types::X, locals stay bare) and the export macros use rustMethodType (fully qualified: shared -> nd_pdk_types::X, primitives -> Rust, locals -> $crate::<pkg>::X). Verified end-to-end by compiling a generated capability that takes types.SongRef directly against the real nd-pdk-types crate. * fix(plugins): canonicalize XTP export refs for renamed shared aliases buildSchemas canonicalized alias-to-canonical references for struct-field $ref targets, but buildExport built export input/output $refs straight from fieldBaseType. A capability whose export used a renamed deprecated alias directly (e.g. type TrackInfo = types.Track with NowPlaying(TrackInfo)) emitted $ref: #/components/schemas/TrackInfo, while the component is emitted under the canonical name Track — a dangling export reference. Lift the alias-to-canonical map into GenerateSchema (buildAliasToCanonical) and apply it to export refs via canonicalRefName, the same resolution already used for field properties. * fix(plugins): route shared macro types through $crate for plugin builds When a capability method used a shared type directly, the generated export macro named the type as nd_pdk_types::SongRef. The macro expands in the downstream plugin crate, which depends on the umbrella nd-pdk crate and not on nd-pdk-types directly, so that path is unresolvable there and the plugin fails to build. rustMethodType (macro-facing) now emits $crate::types::X, and the generated nd-pdk-capabilities lib.rs re-exports nd_pdk_types as types so $crate resolves it. Trait signatures keep nd_pdk_types::X since they live in nd-pdk-capabilities, which has the direct dependency. Verified end-to-end: a plugin crate depending only on the umbrella that uses a capability with a direct types.X method now compiles via the macro. * fix(plugins): add nd-pdk-types dependency to the Rust host crate When a host service uses a shared type, ndpgen emits nd_pdk_types::X into the generated nd-pdk-host client wrappers, but the host crate's manifest did not depend on nd-pdk-types, so the crate failed to compile with 'unresolved module nd_pdk_types'. Host client wrappers are plain functions resolved in the host crate's own context (not macros expanded downstream), so a direct dependency is the right fix. Add nd-pdk-types = { path = "../nd-pdk-types" } to nd-pdk-host, mirroring nd-pdk-capabilities. Found while auditing all Rust paths against the realistic crate topology after the capability-side $crate fix; verified by generating a host service with a shared-type return and confirming cargo check fails before and passes after. * fix(plugins): resolve shared aliases in Rust host signatures The Rust host client rendered method params and returns through RustTypeWithStructs, which only consults KnownStructs. A host service using a shared alias in a signature (e.g. type Track = types.Track plus MatchSongs(...) ([]Track, error)) therefore emitted a bare Vec<Track>, but the client template emits no Track alias or import, so the generated nd-pdk-host crate did not compile. Only struct fields went through the shared map. rustType/rustParamType now use the shared map too (RustTypeWithShared / RustParamTypeWithShared), so an aliased param/return resolves to its canonical nd_pdk_types::X path, matching field handling. Verified by generating a host service returning a shared alias and confirming cargo check fails before and passes after.
2026-06-29 21:20:33 -04:00
ArtistRef:
description: |-
ArtistRef is the minimal information a plugin returns for Navidrome to match an
artist against the library. It is a reference, not a full artist entity: it
feat(plugins): expose the song Matcher as a host service (#5643) * feat(plugins): add public Track and Artist DTOs for host services * feat(plugins): add Matcher host-service interface and MatchSong DTO * feat(plugins): generate Matcher host wrappers, PDK clients, and matcher permission * feat(plugins): implement Matcher host service and MediaFile-to-Track converter Also fixes an ndpgen bug where ParseDirectory parsed each host-service file in isolation, so a service method referencing a struct defined in another file of the same package (host.Track in track.go) could not be resolved. ParseDirectory now collects package-wide structs in a first pass, mirroring ParseCapabilities; PDK clients regenerated cleanly via make gen. * feat(plugins): register Matcher host service in the manager * test(plugins): add Matcher host service integration test plugin * refactor(plugins): simplify matcher converter and parser file collection - toTrack: use gg.V for nil-able field derefs and slice.Map for genres/ participants, removing the repeated nil-guard blocks and inner loop - manager_loader: drop the redundant ds==nil guard (loadEnabledPlugins already gates a nil DataStore), matching the other service entries - ndpgen parser: extract collectGoFiles, shared by ParseDirectory and ParseCapabilities instead of duplicating the file-filter loop * fix(plugins): keep nullable Track numerics as pointers ReplayGain values, BitDepth, and BPM are nullable in model.MediaFile, and 0 is a valid measured ReplayGain value. Flattening them to value types with omitempty made a real 0 indistinguishable from absent. Model them as *float64 /*int32 so plugins can tell 'no data' from a measured 0. Regenerated PDK clients; converter passes the model pointers through (RG) or maps *int->*int32 (BitDepth/BPM). * refactor(plugins): trim redundant pass labels in ndpgen ParseDirectory The function doc already explains the two-pass approach; the inline labels restated it. Reduce to bare waypoints. * fix(plugins): gate Track.Path on library filesystem permission MatchSongs copied mf.Path into every result unconditionally, letting a plugin with only the matcher permission enumerate on-disk file paths by matching known songs. Gate Path behind library.filesystem, matching the Library host service. toTrack is now a method carrying the permission flag. * refactor(plugins): align MatchSong JSON casing and parse Go files once - MatchSong: artistMBID/albumMBID JSON tags -> artistMbid/albumMbid so the Go wire format matches the Rust SDK's camelCase serialization (cross-SDK fix) - MatchSongs doc reworded to language-neutral 'empty (absent)' so generated Rust/Python client docs no longer say Go-specific 'nil' - ndpgen: parse each package file once (parseGoFiles) and reuse the ASTs across both passes in ParseDirectory and ParseCapabilities, instead of re-parsing * refactor(plugins): use shared types for Matcher host service Move the Matcher host service onto the shared plugins/types package instead of the host-local MatchSong and Track structs. MatchSongs now takes []types.SongRef and returns []*types.Track, dropping host.MatchSong and moving host.Track (with its host.Artist dependency collapsed onto types.ArtistRef) into plugins/types. ArtistRef gains SortName and SubRole so it can back a track's Participants. SongRef gains a millisecond-precision DurationMs field that supersedes the now deprecated seconds-based Duration, with DurationInMs() resolving the effective value and SetDurationMs() keeping both fields in sync when populating a SongRef to send to a plugin. The ndpgen host-wrapper template only ever imported context, json and extism, so a host service referencing the shared types package produced uncompilable code. Emit the plugins/types import when the service references shared types directly (gated on the existing Service.ImportsSharedTypes), matching the client template, and cover it with GenerateHost tests. This removes the need for host-local re-export aliases. Regenerated the Go/Rust/Python PDK and capability schemas accordingly. * test(plugins): cover SongRef duration and artist conversion Add unit coverage for the new SongRef behavior: SetDurationMs populating both DurationMs and the deprecated seconds field, and the SongRef-to-agents.Song conversion preferring DurationMs over Duration and the Artists list over the scalar Artist/ArtistMBID. Extract the inline SongRef-to-agents.Song closure in MatchSongs into a named toAgentSong function so the conversion can be asserted directly rather than only through the opaque matcher. The end-to-end wire shape of the moved types is already validated by the existing MatcherService integration test, so no new WASM-boundary test is needed. * fix(plugins): harden and unify SongRef-to-agents.Song duration conversion Address findings from a code review of the matcher host service: - DurationInMs now clamps a negative deprecated-seconds value to 0 instead of converting it through uint32, which previously wrapped a value like -1s into a ~49-day duration that corrupted the matcher's duration-proximity tiebreaker. - Replace the unused SetDurationMs(uint32) with SetDuration(seconds float32), which takes the unit callers actually hold (model.MediaFile.Duration is float32 seconds) and centralizes the seconds-to-ms conversion. Wire it into mediaFileToSongRef so outbound SongRefs carry both duration fields in sync. - Make the metadata-agent path use DurationInMs() so every consumer of the shared SongRef honors the DurationMs-over-Duration precedence contract; a plugin sending only DurationMs no longer loses its duration on that path. - Collapse the matcher's duplicate toAgentSong/agentArtists helpers into the existing songRefToAgentSong converter, so there is a single SongRef-to-Song mapping. Tests narrowed to the duration cases, with artist precedence still covered in metadata_agent_test.go. * feat(plugins): allow Matcher host service to scope a match to a user Add an options struct to the Matcher host service so a plugin can run a match as a specific user. When MatchOptions.Username is set, the match is run in that user's context: their favourites and ratings inform the matcher's tiebreaker, and the returned tracks carry that user's per-user annotations (Starred, StarredAt, Rating, PlayCount, PlayDate, added to types.Track). An empty username preserves the previous unscoped behaviour. Cross-user access is gated by the same allowedUsers/allUsers permission the Users and SubsonicAPI host services use: an unknown username, or one the plugin is not permitted to act as, returns an error. User-library access applies automatically once the user is in context (applyLibraryFilter). Independently, results are now restricted to the libraries the plugin itself may access via the precomputed libraryAccess set, dropping any matched track outside that set (the input index stays unmatched) — this applies even without a username and even for an admin-scoped user. core/matcher is unchanged: it already loads and uses annotations and applies user-library filtering from context, so the feature works by deriving the request context and post-filtering by plugin library access in the host adapter. The new opts parameter and the Track annotation fields are propagated to all PDK clients (Go/Rust/Python) by make gen. * fix(plugins): correct Matcher library scope and unify user-access checks Address findings from a code review of the user-scoped Matcher host service: - The plugin-library post-filter previously dropped every match for a plugin that holds only the matcher permission, because library config is tied to the Library permission and a matcher-only plugin has none (empty allowedLibraries, AllLibraries=false). Gate the filter on whether the plugin actually declared the Library permission: matcher-only plugins are no longer library-restricted, while plugins that opt into a library scope are enforced as before. The per-user library filter (applyLibraryFilter) still applies whenever a non-admin user is scoped. - resolveUser collapsed every FindByUsername error (including transient DB failures) into a misleading "not found". Extract a shared userAccess type (alongside libraryAccess) whose resolve() distinguishes model.ErrNotFound from a real backend error and authorizes the user against the allowed set. The Matcher service now uses it, and host_subsonicapi shares the same userAccess type for its permission check (preserving its existing error messages), removing a third divergent copy of the resolve-and-authorize logic. - Document in the matcher tests that the mock MediaFileRepo returns annotations unconditionally, so the unit tests cover the adapter's scoped-flag gating and access checks but not the SQL per-user join. Add tests for the library-permission gating and for surfacing a backend error instead of masking it as not-found. * fix(plugins): require a library scope for Matcher, fail closed Reverse the permissive default introduced when fixing the library post-filter: a Matcher plugin now must be granted a library scope (all libraries, or at least one specific library) and MatchSongs rejects the request with "no libraries configured" when it has none, instead of either silently matching nothing or defaulting to every library. This mirrors how the SubsonicAPI host service requires a user scope (checkPermissions errors with "no users configured" when none is set): the check is a runtime guard via libraryAccess.configured(), needs no manifest changes, and keeps the failure loud rather than silent. The per-match library post-filter then always applies, and the restrictLibraries flag added in the previous commit is removed. * fix(plugins): require library permission for matcher; guard nil user Close the gap where a plugin declaring only the matcher permission loaded successfully but failed every MatchSongs call with "no libraries configured", with no way for an admin to grant a library scope (the library-config UI is gated on the library permission). Add a cross-field manifest rule, mirroring the existing "subsonicapi requires users" rule, so the matcher permission requires the library permission to be declared. A matcher plugin therefore also surfaces the library-config panel and is subject to the existing load/enable-time library configuration gate, making the fail-closed library check reachable and fixable rather than a silent dead end. The test plugin manifest now declares the library permission accordingly. Also restore a defensive nil-user guard in userAccess.resolve: if a DataStore's FindByUsername ever returns (nil, nil) instead of model.ErrNotFound, return a clean "not found" error rather than dereferencing a nil *model.User. * feat(plugins): expose track AverageRating in Matcher results Add AverageRating to the Matcher's Track DTO. Unlike the per-user annotations (Starred, Rating, PlayCount, ...), AverageRating is an aggregate stored on the track itself and is loaded regardless of the request user, so it is populated unconditionally rather than gated on a scoped username. Propagated to the PDK types by make gen. Signed-off-by: Deluan <deluan@navidrome.org> * style(plugins): trim verbose comments in matcher host service Condense the over-long explanatory comments added across the matcher host service to one-liners that state the why, and simplify the ptrInt32/unixPtr helpers to Go 1.26's new(value). No behavior change. * refactor(plugins): pass userAccess into newSubsonicAPIService Move newUserAccess construction to the loader call site so the SubsonicAPI service constructor takes a userAccess value directly, matching newMatcherService. Pure refactor: the service already stored a userAccess internally, so behavior and error messages are unchanged. * fix(plugins): regenerate PDK and drop omitempty from AverageRating Re-run make gen so the generated PDK doc comments match the source comment trimmed in an earlier commit (the source was simplified but the PDK was not regenerated, leaving the committed files stale — a 'generated files up to date' hazard). Also drop omitempty from Track.AverageRating: it is always set (0 when unrated), so it should be present in the payload like the other always-set fields (BirthTime/CreatedAt/UpdatedAt), not dropped at zero. Tag change propagated to the PDK by the same regeneration. * fix(plugins): reject user-scoped match before lookup when plugin has no user scope A matcher plugin requires the library permission but not the users permission, so a matcher-only plugin always has an empty user scope (allUsers=false, no allowed users). MatchSongs still ran FindByUsername for any opts.Username before checking authorization and returned distinguishable errors ('user X not found' vs 'not allowed to act as user X'), letting such a plugin enumerate account names from the error text. Guard userAccess.resolve to reject with a single fixed error before the lookup when the plugin has no user scope, mirroring how the SubsonicAPI service short-circuits with 'no users configured'. The unscoped match path (no username) is unaffected, so matcher-only plugins still match normally. * fix(plugins): run unscoped matcher as admin, not the inherited request user A matcher host call can arrive on a context that already carries a request user (e.g. a plugin capability invoked while serving that user's request — extism propagates the call context into host functions). With no opts.Username, MatchSongs passed that context straight through, so the media-file repository applied the caller's library filter and per-user annotation ranking to an explicitly unscoped match. Set the user context explicitly: a username scopes to that user (overriding any inherited one), and an unscoped match runs under adminContext so only the plugin's own library scope constrains results. Adds tests using a context-capturing DataStore to assert the user the matcher resolves in both cases. * chore(plugins): drop the generated Python matcher PDK The Python plugin PDK is no longer supported (ndpgen generates only Go and Rust clients), so remove the stale generated nd_host_matcher.py rather than leave a client that drifts from the host interface. * docs(plugins): deprecate SongRef.Artist/ArtistMBID in favor of Artists Mark the scalar single-artist fields deprecated; Artists (the ArtistRef list) is the preferred way to supply artist data and already takes precedence for matching. Propagated to the PDK and capability schemas by make gen. * refactor(plugins): flatten Track.Participants and add Role to ArtistRef Change Track.Participants from map[role][]ArtistRef to a flat []ArtistRef, and give ArtistRef a Role field (the participation category: artist/composer/performer/...) alongside SubRole (a specialization within a role, e.g. the instrument for a performer). In the flat list each entry now self-describes its role rather than relying on a map key, matching how SongRef.Artists is already a flat list; the converter tags each entry with its role and emits them in a stable role order. Propagated to the PDK and capability schemas by make gen. --------- Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-01 11:19:15 -04:00
carries only matching keys (name and optional internal/MusicBrainz IDs) plus a
few projection fields used when describing a track's participants, never
feat(plugins): share plugin DTOs via a types package (#5655) * refactor(plugins): remove Python PDK generation from ndpgen * feat(plugins): parse Go type aliases distinctly in ndpgen * feat(plugins): resolve shared-type aliases against a registry in ndpgen * fix(plugins): resolve host-service shared aliases package-wide Mirror the capability approach in ParseDirectoryWithShared: do a first pass over all package files to build a package-wide alias map, then pass it into parseServiceFile so that a shared-type alias declared in a sibling file is visible when resolving types in the service interface file. Add a focused test that writes the alias in one file and the hostservice in another, confirming RED before the fix and GREEN after. Also strengthens the existing Task 3 test with an ArtistRef.Target assertion. * feat(plugins): add ndpgen -shared-types mode for the Go types package * feat(plugins): generate the nd-pdk-types Rust crate from -shared-types * feat(plugins): inject types import and emit deprecated aliases in Go output * feat(plugins): emit deprecated Rust aliases to the shared types crate * feat(plugins): inline shared-type shapes into XTP schemas * feat(plugins): add nd-pdk-types crate and wire dependents * feat(plugins): move shared capability types to plugins/types with deprecated aliases * fix(plugins): point Rust deprecated-alias note at the replacement type * fix(plugins): include shared aliases in KnownStructs so Rust fields keep their type Capability.KnownStructs() and Service.KnownStructs() previously only registered names from .Structs. After the shared-types migration, types like ArtistRef/TrackInfo/SongRef live in .SharedAliases instead, so ToRustTypeWithStructs could not find them and fell back to serde_json::Value for every struct field referencing a shared type. Add the shared-alias names to the knownStructs map in both methods. Regenerate the Rust capability files; track/song/artist fields now render as their named types (TrackInfo, SongRef, ArtistRef, etc.). Add a regression test that verifies a struct field whose type is only in SharedAliases renders as the named type and not serde_json::Value. * docs(plugins): remove stale Python references from ndpgen and plugins READMEs ndpgen no longer has a -python flag; remove it from the usage synopsis, flags table, and defaults note in ndpgen/README.md. Delete the "Python Client Library" section that described its output. plugins/README.md referenced plugins/pdk/python/host/ (deleted) as the source for Python host-service stubs. Remove that paragraph; Python plugins still work via the XTP-schema / extism-py path (see examples/*-py). * refactor(plugins): dedupe ndpgen helpers and tidy shared-type codegen * docs(plugins): restore Python as a supported XTP schema target The ndpgen-generated Python PDK was removed, but the XTP YAML schemas are language-neutral and the XTP CLI still generates Python bindings from them (as the extism-py examples demonstrate). Only the ndpgen Python output was dropped, not Python support itself. * test(plugins): use the shared types package in test plugins The test fixtures referenced the now-deprecated capability aliases (sonicsimilarity.SongRef, metadata.ArtistRef/SongRef). Point them at the canonical types package so our own fixtures don't depend on symbols slated for removal. * refactor(plugins): use the shared types package in host adapters Replace deprecated capabilities.TrackInfo, capabilities.ArtistRef, and capabilities.SongRef aliases with the canonical types.TrackInfo, types.ArtistRef, and types.SongRef from plugins/types. * fix(plugins): reference shared types by canonical path in generated Rust Previously the generator emitted `pub field: SongRef` (the local deprecated alias) for struct fields whose type came from SharedAliases. Refactored ToRustTypeWithStructs into a private toRustType that accepts a shared map, and added ToRustTypeWithShared which resolves shared-alias names to their canonical nd_pdk_types::X path before falling through to the knownStructs check. Both rustCapabilityFuncMap and rustFuncMap now build the shared map from SharedAliases and use it for fieldRustType, so the generated capability files reference nd_pdk_types::SongRef / nd_pdk_types::TrackInfo directly. The deprecated pub type aliases remain in place as the external back-compat surface. Deprecation warning count from cargo build drops to 0. * fix(examples): implement missing Scrobbler.playback_report in Rust examples The Scrobbler trait gained a playback_report method but the two Rust example plugins (webhook-rs and discord-rich-presence-rs) were not updated, causing E0046 compile errors. Added the missing fn playback_report to both: webhook-rs logs and returns Ok(()) mirroring its now_playing handler; discord-rich-presence-rs is a no-op since Discord presence does not need playback reports. make all-rust now exits 0. * refactor(plugins): point Rust deprecation notes at the nd_pdk::types umbrella path Plugin authors depend on the nd-pdk umbrella crate, which re-exports nd_pdk_types as 'types', so the migration target they should type is nd_pdk::types::X. The alias target stays nd_pdk_types::X (the real path inside nd-pdk-capabilities). * fix(plugins): error when a shared-type alias can't be resolved against the registry * refactor(plugins): parse each Go source file once in ndpgen * fix(plugins): correct ndpgen review nits (flag name, unused dep, docs) * refactor(plugins): drop the now-unused path param from parseServiceFile * refactor(plugins): use shared types directly, rename TrackInfo to Track Capability interfaces now reference the shared `types` package by qualified name (types.Track, types.SongRef, types.ArtistRef) instead of the package-local deprecated aliases, and the shared TrackInfo type is renamed to Track to match its role as the plugin-facing projection of a library media file. The deprecated bare aliases (scrobbler.TrackInfo, metadata.ArtistRef, sonicsimilarity.SongRef, etc.) are kept as re-exports so existing plugins keep compiling, with a deprecation warning steering them to the canonical types. To support this, ndpgen now resolves qualified types.X references: it collects them during type discovery, maps each used canonical type back to its declared deprecated alias for re-export, emits nd_pdk_types::X paths in Rust, and names the XTP schema components by their canonical type. Regenerated the Go and Rust PDK and the XTP schemas, and added generator tests covering the qualified-ref path. Also adds clarifying doc comments to the shared types. * refactor(plugins): extract shared types selector into a named const Replace the "types." string literal that detects and strips the shared types package selector with a single sharedTypesPrefix constant across the ndpgen generator (parser, types, generator, xtp_schema), giving the package one source of truth for the selector. Also restore the single reused scratch map (cleared each iteration) in the resolveSharedAliases BFS instead of allocating a fresh map per shared-struct field, matching the prior implementation. Pure cleanup from a /simplify pass: regeneration produces byte-for-byte identical Go, Rust, and XTP output. * refactor(plugins): keep TrackInfo in the capability package for now Move the track type back out of the shared plugins/types package: it is again defined inline as TrackInfo in plugins/capabilities/scrobbler.go and referenced directly by the scrobbler and lyrics capabilities, reverting the rename to types.Track. The host helper is renamed back to mediaFileToTrackInfo and now returns capabilities.TrackInfo. SongRef and ArtistRef stay in the shared types package; TrackInfo keeps using types.ArtistRef for its artist lists. This type is expected to be reshaped in upcoming work, so leaving it in the capability package avoids churning the shared types twice. Regenerated the Go and Rust PDK and the XTP schemas accordingly. * fix(plugins): emit the Go types import for direct shared-type refs ndpgen's Capability/Service.ImportsSharedTypes only reported a shared-types dependency when a deprecated re-export alias (type X = types.X) was declared. A struct field referencing the canonical form directly (e.g. types.SongRef) with no such alias produced an empty SharedAliases slice, so the Go templates skipped the import while still emitting fields/signatures using types.SongRef — leaving generated PDK code for new shared DTOs uncompilable unless an otherwise unnecessary alias was added. ImportsSharedTypes now also returns true when any struct field references the types. package by qualified name, via a new structsReferenceSharedTypes helper that reuses collectReferencedTypes (so []types.X and map[...]types.X are covered too). * fix(plugins): preserve base64 encoding for shared byte fields in Rust The Rust shared-types crate template rendered a []byte field as a plain Vec<u8> without the base64_bytes serde override used by the capability/client templates. Go's encoding/json serializes []byte as a base64 string, so a Rust plugin using nd_pdk::types would have serialized an array of numbers instead of the wire format the Go/server side expects. GenerateSharedTypesRust now registers the base64_bytes partial and passes a HasByteFields flag (new anyFieldIsByteSlice helper); types.rs.tmpl emits the base64_bytes module and a #[serde(with = "base64_bytes")] attribute on []byte fields, mirroring the capability template. * fix(plugins): include directly-referenced shared types in XTP schemas buildSchemas registered shared types into the schema components only by iterating cap.SharedAliases, which records deprecated re-export aliases. A capability that referenced a shared DTO solely as types.Foo (no declared alias) therefore never got Foo into the component set, so the self-contained XTP schema rendered the field as a generic object (or emitted a dangling $ref), breaking the direct shared-type use case enabled by -shared. resolveSharedAliases now also returns the resolved shapes of every used shared type (alias or not); these are carried on the new Capability.SharedTypes field and registered by buildSchemas alongside SharedAliases. Validated end-to-end with the xtp CLI: a direct types.Foo reference now produces a proper component plus a $ref, so xtp generates a typed struct instead of an untyped serde_json::Map. * fix(plugins): resolve renamed shared aliases to canonical schema refs When a deprecated alias renames its canonical type (e.g. type TrackInfo = types.Track) and a capability field is typed with the alias name (TrackInfo), buildProperty emitted a $ref to #/components/schemas/TrackInfo. Components are keyed by the canonical name (Track), so no TrackInfo component was emitted, leaving a dangling reference that crashes the xtp code generator. buildSchemas now builds an alias->canonical map; buildProperty (and the slice item path) resolves $ref targets through it, and a used alias name marks its canonical component used so it is emitted. Validated with the xtp CLI: the renamed-alias schema previously crashed xtp and now generates cleanly. * fix(plugins): detect shared types used directly in method signatures ImportsSharedTypes only inspected struct fields, so a capability method using a shared type directly in its signature (e.g. types.SongRef as input/output rather than inside a local struct) was not detected. The generated Go templates still rendered the provider/export signatures with types.SongRef, so the capability package omitted the types import and failed to compile; the same gap applied to service params/returns. ImportsSharedTypes now also scans capability method input/output types and service method params/returns, via a typeReferencesSharedTypes helper that reuses collectReferencedTypes (covering pointer/slice/map wrappers). * fix(plugins): add base64 dependency to the shared Rust types crate When a shared DTO has a []byte field, ndpgen emits the base64_bytes serde helper and use base64::... imports into nd-pdk-types/src/lib.rs, but the crate manifest declared only serde. In that case make gen produced a crate that failed to compile with 'unresolved module base64'. Add base64 = "0.22" (matching nd-pdk-capabilities) so the generated shared types crate compiles whenever a []byte field is present. Verified by generating a shared crate with a []byte field and confirming cargo check fails before and passes after. * fix(plugins): translate shared method types in generated Rust A capability method using a shared DTO directly as input/output (e.g. types.SongRef) was passed through rustOutputType unchanged, so the Rust template emitted invalid trait and extism_pdk::Json<$crate::pkg::types.SongRef> signatures that do not compile. Method input/output types now resolve through the shared registry: trait signatures use rustTraitType (shared -> nd_pdk_types::X, locals stay bare) and the export macros use rustMethodType (fully qualified: shared -> nd_pdk_types::X, primitives -> Rust, locals -> $crate::<pkg>::X). Verified end-to-end by compiling a generated capability that takes types.SongRef directly against the real nd-pdk-types crate. * fix(plugins): canonicalize XTP export refs for renamed shared aliases buildSchemas canonicalized alias-to-canonical references for struct-field $ref targets, but buildExport built export input/output $refs straight from fieldBaseType. A capability whose export used a renamed deprecated alias directly (e.g. type TrackInfo = types.Track with NowPlaying(TrackInfo)) emitted $ref: #/components/schemas/TrackInfo, while the component is emitted under the canonical name Track — a dangling export reference. Lift the alias-to-canonical map into GenerateSchema (buildAliasToCanonical) and apply it to export refs via canonicalRefName, the same resolution already used for field properties. * fix(plugins): route shared macro types through $crate for plugin builds When a capability method used a shared type directly, the generated export macro named the type as nd_pdk_types::SongRef. The macro expands in the downstream plugin crate, which depends on the umbrella nd-pdk crate and not on nd-pdk-types directly, so that path is unresolvable there and the plugin fails to build. rustMethodType (macro-facing) now emits $crate::types::X, and the generated nd-pdk-capabilities lib.rs re-exports nd_pdk_types as types so $crate resolves it. Trait signatures keep nd_pdk_types::X since they live in nd-pdk-capabilities, which has the direct dependency. Verified end-to-end: a plugin crate depending only on the umbrella that uses a capability with a direct types.X method now compiles via the macro. * fix(plugins): add nd-pdk-types dependency to the Rust host crate When a host service uses a shared type, ndpgen emits nd_pdk_types::X into the generated nd-pdk-host client wrappers, but the host crate's manifest did not depend on nd-pdk-types, so the crate failed to compile with 'unresolved module nd_pdk_types'. Host client wrappers are plain functions resolved in the host crate's own context (not macros expanded downstream), so a direct dependency is the right fix. Add nd-pdk-types = { path = "../nd-pdk-types" } to nd-pdk-host, mirroring nd-pdk-capabilities. Found while auditing all Rust paths against the realistic crate topology after the capability-side $crate fix; verified by generating a host service with a shared-type return and confirming cargo check fails before and passes after. * fix(plugins): resolve shared aliases in Rust host signatures The Rust host client rendered method params and returns through RustTypeWithStructs, which only consults KnownStructs. A host service using a shared alias in a signature (e.g. type Track = types.Track plus MatchSongs(...) ([]Track, error)) therefore emitted a bare Vec<Track>, but the client template emits no Track alias or import, so the generated nd-pdk-host crate did not compile. Only struct fields went through the shared map. rustType/rustParamType now use the shared map too (RustTypeWithShared / RustParamTypeWithShared), so an aliased param/return resolves to its canonical nd_pdk_types::X path, matching field handling. Verified by generating a host service returning a shared alias and confirming cargo check fails before and passes after.
2026-06-29 21:20:33 -04:00
descriptive data such as biographies or images.
properties:
id:
type: string
description: ID is the internal Navidrome artist ID (if known).
name:
type: string
description: Name is the artist name.
mbid:
type: string
description: MBID is the MusicBrainz ID for the artist.
feat(plugins): expose the song Matcher as a host service (#5643) * feat(plugins): add public Track and Artist DTOs for host services * feat(plugins): add Matcher host-service interface and MatchSong DTO * feat(plugins): generate Matcher host wrappers, PDK clients, and matcher permission * feat(plugins): implement Matcher host service and MediaFile-to-Track converter Also fixes an ndpgen bug where ParseDirectory parsed each host-service file in isolation, so a service method referencing a struct defined in another file of the same package (host.Track in track.go) could not be resolved. ParseDirectory now collects package-wide structs in a first pass, mirroring ParseCapabilities; PDK clients regenerated cleanly via make gen. * feat(plugins): register Matcher host service in the manager * test(plugins): add Matcher host service integration test plugin * refactor(plugins): simplify matcher converter and parser file collection - toTrack: use gg.V for nil-able field derefs and slice.Map for genres/ participants, removing the repeated nil-guard blocks and inner loop - manager_loader: drop the redundant ds==nil guard (loadEnabledPlugins already gates a nil DataStore), matching the other service entries - ndpgen parser: extract collectGoFiles, shared by ParseDirectory and ParseCapabilities instead of duplicating the file-filter loop * fix(plugins): keep nullable Track numerics as pointers ReplayGain values, BitDepth, and BPM are nullable in model.MediaFile, and 0 is a valid measured ReplayGain value. Flattening them to value types with omitempty made a real 0 indistinguishable from absent. Model them as *float64 /*int32 so plugins can tell 'no data' from a measured 0. Regenerated PDK clients; converter passes the model pointers through (RG) or maps *int->*int32 (BitDepth/BPM). * refactor(plugins): trim redundant pass labels in ndpgen ParseDirectory The function doc already explains the two-pass approach; the inline labels restated it. Reduce to bare waypoints. * fix(plugins): gate Track.Path on library filesystem permission MatchSongs copied mf.Path into every result unconditionally, letting a plugin with only the matcher permission enumerate on-disk file paths by matching known songs. Gate Path behind library.filesystem, matching the Library host service. toTrack is now a method carrying the permission flag. * refactor(plugins): align MatchSong JSON casing and parse Go files once - MatchSong: artistMBID/albumMBID JSON tags -> artistMbid/albumMbid so the Go wire format matches the Rust SDK's camelCase serialization (cross-SDK fix) - MatchSongs doc reworded to language-neutral 'empty (absent)' so generated Rust/Python client docs no longer say Go-specific 'nil' - ndpgen: parse each package file once (parseGoFiles) and reuse the ASTs across both passes in ParseDirectory and ParseCapabilities, instead of re-parsing * refactor(plugins): use shared types for Matcher host service Move the Matcher host service onto the shared plugins/types package instead of the host-local MatchSong and Track structs. MatchSongs now takes []types.SongRef and returns []*types.Track, dropping host.MatchSong and moving host.Track (with its host.Artist dependency collapsed onto types.ArtistRef) into plugins/types. ArtistRef gains SortName and SubRole so it can back a track's Participants. SongRef gains a millisecond-precision DurationMs field that supersedes the now deprecated seconds-based Duration, with DurationInMs() resolving the effective value and SetDurationMs() keeping both fields in sync when populating a SongRef to send to a plugin. The ndpgen host-wrapper template only ever imported context, json and extism, so a host service referencing the shared types package produced uncompilable code. Emit the plugins/types import when the service references shared types directly (gated on the existing Service.ImportsSharedTypes), matching the client template, and cover it with GenerateHost tests. This removes the need for host-local re-export aliases. Regenerated the Go/Rust/Python PDK and capability schemas accordingly. * test(plugins): cover SongRef duration and artist conversion Add unit coverage for the new SongRef behavior: SetDurationMs populating both DurationMs and the deprecated seconds field, and the SongRef-to-agents.Song conversion preferring DurationMs over Duration and the Artists list over the scalar Artist/ArtistMBID. Extract the inline SongRef-to-agents.Song closure in MatchSongs into a named toAgentSong function so the conversion can be asserted directly rather than only through the opaque matcher. The end-to-end wire shape of the moved types is already validated by the existing MatcherService integration test, so no new WASM-boundary test is needed. * fix(plugins): harden and unify SongRef-to-agents.Song duration conversion Address findings from a code review of the matcher host service: - DurationInMs now clamps a negative deprecated-seconds value to 0 instead of converting it through uint32, which previously wrapped a value like -1s into a ~49-day duration that corrupted the matcher's duration-proximity tiebreaker. - Replace the unused SetDurationMs(uint32) with SetDuration(seconds float32), which takes the unit callers actually hold (model.MediaFile.Duration is float32 seconds) and centralizes the seconds-to-ms conversion. Wire it into mediaFileToSongRef so outbound SongRefs carry both duration fields in sync. - Make the metadata-agent path use DurationInMs() so every consumer of the shared SongRef honors the DurationMs-over-Duration precedence contract; a plugin sending only DurationMs no longer loses its duration on that path. - Collapse the matcher's duplicate toAgentSong/agentArtists helpers into the existing songRefToAgentSong converter, so there is a single SongRef-to-Song mapping. Tests narrowed to the duration cases, with artist precedence still covered in metadata_agent_test.go. * feat(plugins): allow Matcher host service to scope a match to a user Add an options struct to the Matcher host service so a plugin can run a match as a specific user. When MatchOptions.Username is set, the match is run in that user's context: their favourites and ratings inform the matcher's tiebreaker, and the returned tracks carry that user's per-user annotations (Starred, StarredAt, Rating, PlayCount, PlayDate, added to types.Track). An empty username preserves the previous unscoped behaviour. Cross-user access is gated by the same allowedUsers/allUsers permission the Users and SubsonicAPI host services use: an unknown username, or one the plugin is not permitted to act as, returns an error. User-library access applies automatically once the user is in context (applyLibraryFilter). Independently, results are now restricted to the libraries the plugin itself may access via the precomputed libraryAccess set, dropping any matched track outside that set (the input index stays unmatched) — this applies even without a username and even for an admin-scoped user. core/matcher is unchanged: it already loads and uses annotations and applies user-library filtering from context, so the feature works by deriving the request context and post-filtering by plugin library access in the host adapter. The new opts parameter and the Track annotation fields are propagated to all PDK clients (Go/Rust/Python) by make gen. * fix(plugins): correct Matcher library scope and unify user-access checks Address findings from a code review of the user-scoped Matcher host service: - The plugin-library post-filter previously dropped every match for a plugin that holds only the matcher permission, because library config is tied to the Library permission and a matcher-only plugin has none (empty allowedLibraries, AllLibraries=false). Gate the filter on whether the plugin actually declared the Library permission: matcher-only plugins are no longer library-restricted, while plugins that opt into a library scope are enforced as before. The per-user library filter (applyLibraryFilter) still applies whenever a non-admin user is scoped. - resolveUser collapsed every FindByUsername error (including transient DB failures) into a misleading "not found". Extract a shared userAccess type (alongside libraryAccess) whose resolve() distinguishes model.ErrNotFound from a real backend error and authorizes the user against the allowed set. The Matcher service now uses it, and host_subsonicapi shares the same userAccess type for its permission check (preserving its existing error messages), removing a third divergent copy of the resolve-and-authorize logic. - Document in the matcher tests that the mock MediaFileRepo returns annotations unconditionally, so the unit tests cover the adapter's scoped-flag gating and access checks but not the SQL per-user join. Add tests for the library-permission gating and for surfacing a backend error instead of masking it as not-found. * fix(plugins): require a library scope for Matcher, fail closed Reverse the permissive default introduced when fixing the library post-filter: a Matcher plugin now must be granted a library scope (all libraries, or at least one specific library) and MatchSongs rejects the request with "no libraries configured" when it has none, instead of either silently matching nothing or defaulting to every library. This mirrors how the SubsonicAPI host service requires a user scope (checkPermissions errors with "no users configured" when none is set): the check is a runtime guard via libraryAccess.configured(), needs no manifest changes, and keeps the failure loud rather than silent. The per-match library post-filter then always applies, and the restrictLibraries flag added in the previous commit is removed. * fix(plugins): require library permission for matcher; guard nil user Close the gap where a plugin declaring only the matcher permission loaded successfully but failed every MatchSongs call with "no libraries configured", with no way for an admin to grant a library scope (the library-config UI is gated on the library permission). Add a cross-field manifest rule, mirroring the existing "subsonicapi requires users" rule, so the matcher permission requires the library permission to be declared. A matcher plugin therefore also surfaces the library-config panel and is subject to the existing load/enable-time library configuration gate, making the fail-closed library check reachable and fixable rather than a silent dead end. The test plugin manifest now declares the library permission accordingly. Also restore a defensive nil-user guard in userAccess.resolve: if a DataStore's FindByUsername ever returns (nil, nil) instead of model.ErrNotFound, return a clean "not found" error rather than dereferencing a nil *model.User. * feat(plugins): expose track AverageRating in Matcher results Add AverageRating to the Matcher's Track DTO. Unlike the per-user annotations (Starred, Rating, PlayCount, ...), AverageRating is an aggregate stored on the track itself and is loaded regardless of the request user, so it is populated unconditionally rather than gated on a scoped username. Propagated to the PDK types by make gen. Signed-off-by: Deluan <deluan@navidrome.org> * style(plugins): trim verbose comments in matcher host service Condense the over-long explanatory comments added across the matcher host service to one-liners that state the why, and simplify the ptrInt32/unixPtr helpers to Go 1.26's new(value). No behavior change. * refactor(plugins): pass userAccess into newSubsonicAPIService Move newUserAccess construction to the loader call site so the SubsonicAPI service constructor takes a userAccess value directly, matching newMatcherService. Pure refactor: the service already stored a userAccess internally, so behavior and error messages are unchanged. * fix(plugins): regenerate PDK and drop omitempty from AverageRating Re-run make gen so the generated PDK doc comments match the source comment trimmed in an earlier commit (the source was simplified but the PDK was not regenerated, leaving the committed files stale — a 'generated files up to date' hazard). Also drop omitempty from Track.AverageRating: it is always set (0 when unrated), so it should be present in the payload like the other always-set fields (BirthTime/CreatedAt/UpdatedAt), not dropped at zero. Tag change propagated to the PDK by the same regeneration. * fix(plugins): reject user-scoped match before lookup when plugin has no user scope A matcher plugin requires the library permission but not the users permission, so a matcher-only plugin always has an empty user scope (allUsers=false, no allowed users). MatchSongs still ran FindByUsername for any opts.Username before checking authorization and returned distinguishable errors ('user X not found' vs 'not allowed to act as user X'), letting such a plugin enumerate account names from the error text. Guard userAccess.resolve to reject with a single fixed error before the lookup when the plugin has no user scope, mirroring how the SubsonicAPI service short-circuits with 'no users configured'. The unscoped match path (no username) is unaffected, so matcher-only plugins still match normally. * fix(plugins): run unscoped matcher as admin, not the inherited request user A matcher host call can arrive on a context that already carries a request user (e.g. a plugin capability invoked while serving that user's request — extism propagates the call context into host functions). With no opts.Username, MatchSongs passed that context straight through, so the media-file repository applied the caller's library filter and per-user annotation ranking to an explicitly unscoped match. Set the user context explicitly: a username scopes to that user (overriding any inherited one), and an unscoped match runs under adminContext so only the plugin's own library scope constrains results. Adds tests using a context-capturing DataStore to assert the user the matcher resolves in both cases. * chore(plugins): drop the generated Python matcher PDK The Python plugin PDK is no longer supported (ndpgen generates only Go and Rust clients), so remove the stale generated nd_host_matcher.py rather than leave a client that drifts from the host interface. * docs(plugins): deprecate SongRef.Artist/ArtistMBID in favor of Artists Mark the scalar single-artist fields deprecated; Artists (the ArtistRef list) is the preferred way to supply artist data and already takes precedence for matching. Propagated to the PDK and capability schemas by make gen. * refactor(plugins): flatten Track.Participants and add Role to ArtistRef Change Track.Participants from map[role][]ArtistRef to a flat []ArtistRef, and give ArtistRef a Role field (the participation category: artist/composer/performer/...) alongside SubRole (a specialization within a role, e.g. the instrument for a performer). In the flat list each entry now self-describes its role rather than relying on a map key, matching how SongRef.Artists is already a flat list; the converter tags each entry with its role and emits them in a stable role order. Propagated to the PDK and capability schemas by make gen. --------- Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-01 11:19:15 -04:00
sortName:
type: string
description: SortName is the artist name used for sorting (if known).
role:
type: string
description: Role is the participation category (e.g. "artist", "composer", "performer").
subRole:
type: string
description: SubRole is a specialization within Role (e.g. the instrument for a performer).
feat(plugins): share plugin DTOs via a types package (#5655) * refactor(plugins): remove Python PDK generation from ndpgen * feat(plugins): parse Go type aliases distinctly in ndpgen * feat(plugins): resolve shared-type aliases against a registry in ndpgen * fix(plugins): resolve host-service shared aliases package-wide Mirror the capability approach in ParseDirectoryWithShared: do a first pass over all package files to build a package-wide alias map, then pass it into parseServiceFile so that a shared-type alias declared in a sibling file is visible when resolving types in the service interface file. Add a focused test that writes the alias in one file and the hostservice in another, confirming RED before the fix and GREEN after. Also strengthens the existing Task 3 test with an ArtistRef.Target assertion. * feat(plugins): add ndpgen -shared-types mode for the Go types package * feat(plugins): generate the nd-pdk-types Rust crate from -shared-types * feat(plugins): inject types import and emit deprecated aliases in Go output * feat(plugins): emit deprecated Rust aliases to the shared types crate * feat(plugins): inline shared-type shapes into XTP schemas * feat(plugins): add nd-pdk-types crate and wire dependents * feat(plugins): move shared capability types to plugins/types with deprecated aliases * fix(plugins): point Rust deprecated-alias note at the replacement type * fix(plugins): include shared aliases in KnownStructs so Rust fields keep their type Capability.KnownStructs() and Service.KnownStructs() previously only registered names from .Structs. After the shared-types migration, types like ArtistRef/TrackInfo/SongRef live in .SharedAliases instead, so ToRustTypeWithStructs could not find them and fell back to serde_json::Value for every struct field referencing a shared type. Add the shared-alias names to the knownStructs map in both methods. Regenerate the Rust capability files; track/song/artist fields now render as their named types (TrackInfo, SongRef, ArtistRef, etc.). Add a regression test that verifies a struct field whose type is only in SharedAliases renders as the named type and not serde_json::Value. * docs(plugins): remove stale Python references from ndpgen and plugins READMEs ndpgen no longer has a -python flag; remove it from the usage synopsis, flags table, and defaults note in ndpgen/README.md. Delete the "Python Client Library" section that described its output. plugins/README.md referenced plugins/pdk/python/host/ (deleted) as the source for Python host-service stubs. Remove that paragraph; Python plugins still work via the XTP-schema / extism-py path (see examples/*-py). * refactor(plugins): dedupe ndpgen helpers and tidy shared-type codegen * docs(plugins): restore Python as a supported XTP schema target The ndpgen-generated Python PDK was removed, but the XTP YAML schemas are language-neutral and the XTP CLI still generates Python bindings from them (as the extism-py examples demonstrate). Only the ndpgen Python output was dropped, not Python support itself. * test(plugins): use the shared types package in test plugins The test fixtures referenced the now-deprecated capability aliases (sonicsimilarity.SongRef, metadata.ArtistRef/SongRef). Point them at the canonical types package so our own fixtures don't depend on symbols slated for removal. * refactor(plugins): use the shared types package in host adapters Replace deprecated capabilities.TrackInfo, capabilities.ArtistRef, and capabilities.SongRef aliases with the canonical types.TrackInfo, types.ArtistRef, and types.SongRef from plugins/types. * fix(plugins): reference shared types by canonical path in generated Rust Previously the generator emitted `pub field: SongRef` (the local deprecated alias) for struct fields whose type came from SharedAliases. Refactored ToRustTypeWithStructs into a private toRustType that accepts a shared map, and added ToRustTypeWithShared which resolves shared-alias names to their canonical nd_pdk_types::X path before falling through to the knownStructs check. Both rustCapabilityFuncMap and rustFuncMap now build the shared map from SharedAliases and use it for fieldRustType, so the generated capability files reference nd_pdk_types::SongRef / nd_pdk_types::TrackInfo directly. The deprecated pub type aliases remain in place as the external back-compat surface. Deprecation warning count from cargo build drops to 0. * fix(examples): implement missing Scrobbler.playback_report in Rust examples The Scrobbler trait gained a playback_report method but the two Rust example plugins (webhook-rs and discord-rich-presence-rs) were not updated, causing E0046 compile errors. Added the missing fn playback_report to both: webhook-rs logs and returns Ok(()) mirroring its now_playing handler; discord-rich-presence-rs is a no-op since Discord presence does not need playback reports. make all-rust now exits 0. * refactor(plugins): point Rust deprecation notes at the nd_pdk::types umbrella path Plugin authors depend on the nd-pdk umbrella crate, which re-exports nd_pdk_types as 'types', so the migration target they should type is nd_pdk::types::X. The alias target stays nd_pdk_types::X (the real path inside nd-pdk-capabilities). * fix(plugins): error when a shared-type alias can't be resolved against the registry * refactor(plugins): parse each Go source file once in ndpgen * fix(plugins): correct ndpgen review nits (flag name, unused dep, docs) * refactor(plugins): drop the now-unused path param from parseServiceFile * refactor(plugins): use shared types directly, rename TrackInfo to Track Capability interfaces now reference the shared `types` package by qualified name (types.Track, types.SongRef, types.ArtistRef) instead of the package-local deprecated aliases, and the shared TrackInfo type is renamed to Track to match its role as the plugin-facing projection of a library media file. The deprecated bare aliases (scrobbler.TrackInfo, metadata.ArtistRef, sonicsimilarity.SongRef, etc.) are kept as re-exports so existing plugins keep compiling, with a deprecation warning steering them to the canonical types. To support this, ndpgen now resolves qualified types.X references: it collects them during type discovery, maps each used canonical type back to its declared deprecated alias for re-export, emits nd_pdk_types::X paths in Rust, and names the XTP schema components by their canonical type. Regenerated the Go and Rust PDK and the XTP schemas, and added generator tests covering the qualified-ref path. Also adds clarifying doc comments to the shared types. * refactor(plugins): extract shared types selector into a named const Replace the "types." string literal that detects and strips the shared types package selector with a single sharedTypesPrefix constant across the ndpgen generator (parser, types, generator, xtp_schema), giving the package one source of truth for the selector. Also restore the single reused scratch map (cleared each iteration) in the resolveSharedAliases BFS instead of allocating a fresh map per shared-struct field, matching the prior implementation. Pure cleanup from a /simplify pass: regeneration produces byte-for-byte identical Go, Rust, and XTP output. * refactor(plugins): keep TrackInfo in the capability package for now Move the track type back out of the shared plugins/types package: it is again defined inline as TrackInfo in plugins/capabilities/scrobbler.go and referenced directly by the scrobbler and lyrics capabilities, reverting the rename to types.Track. The host helper is renamed back to mediaFileToTrackInfo and now returns capabilities.TrackInfo. SongRef and ArtistRef stay in the shared types package; TrackInfo keeps using types.ArtistRef for its artist lists. This type is expected to be reshaped in upcoming work, so leaving it in the capability package avoids churning the shared types twice. Regenerated the Go and Rust PDK and the XTP schemas accordingly. * fix(plugins): emit the Go types import for direct shared-type refs ndpgen's Capability/Service.ImportsSharedTypes only reported a shared-types dependency when a deprecated re-export alias (type X = types.X) was declared. A struct field referencing the canonical form directly (e.g. types.SongRef) with no such alias produced an empty SharedAliases slice, so the Go templates skipped the import while still emitting fields/signatures using types.SongRef — leaving generated PDK code for new shared DTOs uncompilable unless an otherwise unnecessary alias was added. ImportsSharedTypes now also returns true when any struct field references the types. package by qualified name, via a new structsReferenceSharedTypes helper that reuses collectReferencedTypes (so []types.X and map[...]types.X are covered too). * fix(plugins): preserve base64 encoding for shared byte fields in Rust The Rust shared-types crate template rendered a []byte field as a plain Vec<u8> without the base64_bytes serde override used by the capability/client templates. Go's encoding/json serializes []byte as a base64 string, so a Rust plugin using nd_pdk::types would have serialized an array of numbers instead of the wire format the Go/server side expects. GenerateSharedTypesRust now registers the base64_bytes partial and passes a HasByteFields flag (new anyFieldIsByteSlice helper); types.rs.tmpl emits the base64_bytes module and a #[serde(with = "base64_bytes")] attribute on []byte fields, mirroring the capability template. * fix(plugins): include directly-referenced shared types in XTP schemas buildSchemas registered shared types into the schema components only by iterating cap.SharedAliases, which records deprecated re-export aliases. A capability that referenced a shared DTO solely as types.Foo (no declared alias) therefore never got Foo into the component set, so the self-contained XTP schema rendered the field as a generic object (or emitted a dangling $ref), breaking the direct shared-type use case enabled by -shared. resolveSharedAliases now also returns the resolved shapes of every used shared type (alias or not); these are carried on the new Capability.SharedTypes field and registered by buildSchemas alongside SharedAliases. Validated end-to-end with the xtp CLI: a direct types.Foo reference now produces a proper component plus a $ref, so xtp generates a typed struct instead of an untyped serde_json::Map. * fix(plugins): resolve renamed shared aliases to canonical schema refs When a deprecated alias renames its canonical type (e.g. type TrackInfo = types.Track) and a capability field is typed with the alias name (TrackInfo), buildProperty emitted a $ref to #/components/schemas/TrackInfo. Components are keyed by the canonical name (Track), so no TrackInfo component was emitted, leaving a dangling reference that crashes the xtp code generator. buildSchemas now builds an alias->canonical map; buildProperty (and the slice item path) resolves $ref targets through it, and a used alias name marks its canonical component used so it is emitted. Validated with the xtp CLI: the renamed-alias schema previously crashed xtp and now generates cleanly. * fix(plugins): detect shared types used directly in method signatures ImportsSharedTypes only inspected struct fields, so a capability method using a shared type directly in its signature (e.g. types.SongRef as input/output rather than inside a local struct) was not detected. The generated Go templates still rendered the provider/export signatures with types.SongRef, so the capability package omitted the types import and failed to compile; the same gap applied to service params/returns. ImportsSharedTypes now also scans capability method input/output types and service method params/returns, via a typeReferencesSharedTypes helper that reuses collectReferencedTypes (covering pointer/slice/map wrappers). * fix(plugins): add base64 dependency to the shared Rust types crate When a shared DTO has a []byte field, ndpgen emits the base64_bytes serde helper and use base64::... imports into nd-pdk-types/src/lib.rs, but the crate manifest declared only serde. In that case make gen produced a crate that failed to compile with 'unresolved module base64'. Add base64 = "0.22" (matching nd-pdk-capabilities) so the generated shared types crate compiles whenever a []byte field is present. Verified by generating a shared crate with a []byte field and confirming cargo check fails before and passes after. * fix(plugins): translate shared method types in generated Rust A capability method using a shared DTO directly as input/output (e.g. types.SongRef) was passed through rustOutputType unchanged, so the Rust template emitted invalid trait and extism_pdk::Json<$crate::pkg::types.SongRef> signatures that do not compile. Method input/output types now resolve through the shared registry: trait signatures use rustTraitType (shared -> nd_pdk_types::X, locals stay bare) and the export macros use rustMethodType (fully qualified: shared -> nd_pdk_types::X, primitives -> Rust, locals -> $crate::<pkg>::X). Verified end-to-end by compiling a generated capability that takes types.SongRef directly against the real nd-pdk-types crate. * fix(plugins): canonicalize XTP export refs for renamed shared aliases buildSchemas canonicalized alias-to-canonical references for struct-field $ref targets, but buildExport built export input/output $refs straight from fieldBaseType. A capability whose export used a renamed deprecated alias directly (e.g. type TrackInfo = types.Track with NowPlaying(TrackInfo)) emitted $ref: #/components/schemas/TrackInfo, while the component is emitted under the canonical name Track — a dangling export reference. Lift the alias-to-canonical map into GenerateSchema (buildAliasToCanonical) and apply it to export refs via canonicalRefName, the same resolution already used for field properties. * fix(plugins): route shared macro types through $crate for plugin builds When a capability method used a shared type directly, the generated export macro named the type as nd_pdk_types::SongRef. The macro expands in the downstream plugin crate, which depends on the umbrella nd-pdk crate and not on nd-pdk-types directly, so that path is unresolvable there and the plugin fails to build. rustMethodType (macro-facing) now emits $crate::types::X, and the generated nd-pdk-capabilities lib.rs re-exports nd_pdk_types as types so $crate resolves it. Trait signatures keep nd_pdk_types::X since they live in nd-pdk-capabilities, which has the direct dependency. Verified end-to-end: a plugin crate depending only on the umbrella that uses a capability with a direct types.X method now compiles via the macro. * fix(plugins): add nd-pdk-types dependency to the Rust host crate When a host service uses a shared type, ndpgen emits nd_pdk_types::X into the generated nd-pdk-host client wrappers, but the host crate's manifest did not depend on nd-pdk-types, so the crate failed to compile with 'unresolved module nd_pdk_types'. Host client wrappers are plain functions resolved in the host crate's own context (not macros expanded downstream), so a direct dependency is the right fix. Add nd-pdk-types = { path = "../nd-pdk-types" } to nd-pdk-host, mirroring nd-pdk-capabilities. Found while auditing all Rust paths against the realistic crate topology after the capability-side $crate fix; verified by generating a host service with a shared-type return and confirming cargo check fails before and passes after. * fix(plugins): resolve shared aliases in Rust host signatures The Rust host client rendered method params and returns through RustTypeWithStructs, which only consults KnownStructs. A host service using a shared alias in a signature (e.g. type Track = types.Track plus MatchSongs(...) ([]Track, error)) therefore emitted a bare Vec<Track>, but the client template emits no Track alias or import, so the generated nd-pdk-host crate did not compile. Only struct fields went through the shared map. rustType/rustParamType now use the shared map too (RustTypeWithShared / RustParamTypeWithShared), so an aliased param/return resolves to its canonical nd_pdk_types::X path, matching field handling. Verified by generating a host service returning a shared alias and confirming cargo check fails before and passes after.
2026-06-29 21:20:33 -04:00
required:
- name