☰
Navidrome 插件示例完全指南:构建、测试与从零编写 Wasm 插件
2026/10/1 17:30:35 网站建设 项目流程
  • 后端
  • 音视频
  • 前端

【免费下载链接】navidrome

🎧 Your Personal Streaming Service

项目地址:https://gitcode.com/gh_mirrors/na/navidrome
点击查看免费下载

导读

本指南以 Navidrome 仓库中的 plugins/examples/README.md 为骨架,系统讲解官方提供的 8 个示例插件(覆盖 Go、Python、Rust 三种语言与 MetadataAgent、Scrobbler、Scheduler、WebSocket 等多种能力),并深入源码级实现细节。读完本文,你将掌握:用make一键构建.ndp插件包、用 Extism CLI 在脱离 Navidrome 的情况下单测能力函数、通过navidrome.toml安装启用插件,以及从 minimal 示例或 XTP CLI 脚手架出发,创建属于你自己的跨语言插件。

Navidrome 的插件体系基于 WebAssembly(Wasm)与 Extism 框架构建,插件在沙箱中运行,通过 插件系统主文档 定义的"能力(Capabilities)+ 宿主服务(Host Services)"模型与 Navidrome 交互。plugins/examples/目录就是官方精心挑选的"标准答案"集合:每个示例都刻意聚焦一到几个核心知识点,是学习插件开发的最佳起点。

示例总览:8 个插件各自演示什么

plugins/examples/下共有 8 个官方示例,按语言与能力维度可以划分为三类:

插件语言能力(Capabilities)核心演示点
minimalGoMetadataAgent最基础的插件结构与注册模式
wikimediaGoMetadataAgentWikidata/Wikipedia 元数据抓取,真实世界级实现
crypto-tickerGoLifecycle、SchedulerCallback、WebSocketCallback实时加密货币行情(演示类)
coverartarchive-pyPythonMetadataAgentCover Art Archive 封面抓取
nowplaying-pyPythonLifecycle、SchedulerCallback定时记录当前播放(Now Playing 日志)
webhook-rsRustScrobbler播放 scrobble 时触发 HTTP Webhook
library-inspector-rsRustLifecycle、SchedulerCallback定时输出音乐库统计信息
discord-rich-presence-rsRustScrobbler、SchedulerCallback、WebSocketCallback与 Discord 集成(Rust 完整案例)

从这张表可以看出官方刻意设计的"矩阵式"覆盖:语言维度上,Go(TinyGo)适合追求开发体验,Rust 适合追求性能与最小二进制,Python 适合快速原型;能力维度上,MetadataAgent(元数据代理)、Scrobbler(刮擦上报)、SchedulerCallback(定时任务)、WebSocketCallback(长连接事件)均有代表;复杂度维度上,从"单文件单函数"的 minimal 到"多能力多宿主服务"的 discord-rich-presence-rs 一应俱全。

补充:能力与宿主服务的对应关系

理解示例前,建议先建立两个基本概念(详见 插件系统主文档):

  • 能力(Capability):插件能做什么,由导出的函数自动检测,一个插件可实现多个能力。例如 MetadataAgent 能力对应nd_get_artist_biography、nd_get_album_images等nd_*导出函数;Scrobbler 能力要求实现nd_scrobbler_is_authorized、nd_scrobbler_now_playing、nd_scrobbler_scrobble、nd_scrobbler_playback_report全部四个方法(均必选)。
  • 宿主服务(Host Service):插件反向调用 Navidrome 提供的 HTTP、Scheduler、Cache、KVStore、Storage、Task、WebSocket、Library、Matcher、Artwork、SubsonicAPI、Config、Users、ScrobbleRetriever 等服务,除 Config 外均需在 manifest 中声明对应权限。

上表中 crypto-ticker 与 discord-rich-presence-rs 之所以标注多项能力,正是多能力、多宿主服务组合的示范(如 Discord 插件同时使用 HTTP、WebSocket、Cache、Scheduler、Artwork、Config 六个宿主服务)。

构建示例插件:前置条件与 Makefile

前置工具链

按语言不同,构建示例需要以下工具:

  • Go 插件:TinyGo 0.30+(推荐,产出更小的 Wasm 二进制;无 TinyGo 时 Makefile 会退化为GOOS=wasip1 GOARCH=wasm go build)
  • Python 插件:extism-py(实验性支持)
  • Rust 插件:Rust 工具链,并安装wasm32-wasip1目标(Rust 示例全部使用 WASI 以支持文件系统等能力)

一键构建全部插件

make all

该命令会为每个插件产出.ndp包文件(即"zip 压缩包内包含manifest.json+plugin.wasm"的插件分发格式)。在 plugins/examples/Makefile 中,all被拆分为三个子目标:all-go、all-python、all-rust(分别对应$(PLUGINS:%=%.ndp)、$(PYTHON_PLUGINS:%=%.ndp)、$(RUST_PLUGINS:%=%.ndp)),因此也可以按语言分批构建。

构建单个插件

make minimal.ndp make wikimedia.ndp make discord-rich-presence-rs.ndp

Makefile 的设计有几个值得留意的工程细节(见 Makefile):

  • 插件自动发现:不依赖手工维护列表。Go 插件通过扫描包含go.mod的目录($(wildcard */go.mod))、Python 插件通过扫描含plugin/__init__.py的目录、Rust 插件通过扫描含Cargo.toml的目录来识别。
  • TinyGo 优先:TINYGO := $(shell command -v tinygo ...),存在 TinyGo 时用tinygo build -target wasip1 -buildmode=c-shared,否则用 Go 官方工具链交叉编译。
  • PDK 变更触发重建:PDK_GO_SOURCES、PDK_PY_SOURCES、PDK_RS_SOURCES递归收集plugins/pdk/下的源码作为依赖,修改 PDK 会自动触发示例重建。
  • .ndp打包规则:Go 插件通过zip -j $@ $*/manifest.json plugin.wasm将 manifest 与 wasm 压入包内(Python 与 Rust 同理)。

另外,不带扩展名直接执行make minimal也会被.PHONY规则映射到make minimal.ndp。清理构建产物使用:

make clean

该目标会删除所有.ndp与.wasm文件,并对 Rust 插件执行cargo clean。

手工构建(不依赖 Makefile)

如果你想脱离 Makefile 手工复现(例如在自己的插件项目中使用),Go 插件的标准流程是:

go mod tidy tinygo build -o plugin.wasm -target wasip1 -buildmode=c-shared . zip -j minimal.ndp manifest.json plugin.wasm

测试插件:两种方式

方式一:Extism CLI(不启动 Navidrome)

任何插件都可以在不运行 Navidrome 的情况下单独测试。步骤是:先从.ndp包中解出plugin.wasm,再用extism call调用其导出函数:

# 解出 wasm(.ndp 本质是 zip) unzip -p minimal.ndp plugin.wasm > minimal.wasm # 调用能力函数(以元数据代理为例) extism call minimal.wasm nd_get_artist_biography --wasi \ --input '{"id":"1","name":"The Beatles"}'

对于需要发起 HTTP 请求的插件(如 wikimedia),必须用--allow-host显式放行目标域名,这与 Navidrome 沙箱的"主机白名单"机制一致:

unzip -p wikimedia.ndp plugin.wasm > wikimedia.wasm extism call wikimedia.wasm nd_get_artist_biography --wasi \ --input '{"id":"1","name":"Yussef Dayes"}' \ --allow-host "query.wikidata.org" \ --allow-host "en.wikipedia.org"

--wasi标志启用 WASI 支持(Go 插件以wasip1目标编译,依赖 WASI 接口);--input传入 JSON 格式的能力函数入参。

方式二:在 Navidrome 内联机测试

  1. 将.ndp文件复制到你的插件目录(默认<data-folder>/plugins/);
  2. 在navidrome.toml中启用插件:
[Plugins] Enabled = true Folder = "/path/to/plugins"
  1. 对元数据代理类插件,把它加入 agents 列表:
Agents = "lastfm,spotify,wikimedia"

补充说明:Agents是有序的,Navidrome 会按顺序询问各代理,因此插件排在越靠前越优先被采用。若Enabled未开启或插件未放入Folder指定目录,插件不会出现在管理界面中。

创建你自己的插件:三种起步路径

方案一:从 minimal 复制改造

cp -r minimal my-plugin cd my-plugin # 编辑 main.go 和 manifest.json tinygo build -o plugin.wasm -target wasip1 -buildmode=c-shared . zip -j my-plugin.ndp manifest.json plugin.wasm

这是最快的起步方式。以 minimal 为例,其核心代码(见 plugins/examples/minimal/main.go)展示了 Navidrome 推荐的Register()注册模式:

package main import ( "github.com/navidrome/navidrome/plugins/pdk/go/metadata" ) // minimalPlugin 实现 metadata provider 接口 type minimalPlugin struct{} // init 中注册插件实现 func init() { metadata.Register(&minimalPlugin{}) } // 编译期断言:确保实现了 ArtistBiographyProvider 接口 var _ metadata.ArtistBiographyProvider = (*minimalPlugin)(nil) // GetArtistBiography 返回占位传记 func (p *minimalPlugin) GetArtistBiography(input metadata.ArtistRequest) (*metadata.ArtistBiographyResponse, error) { return &metadata.ArtistBiographyResponse{ Biography: "This is a placeholder biography for " + input.Name + ".", }, nil } func main() {}

配套的 manifest.json 只有四个字段,这也是.ndp包元数据的最低要求:

{ "name": "Minimal Example", "author": "Navidrome", "version": "1.0.0", "description": "A minimal example plugin" }

关于metadata.Register()模式,需要理解两点(详见 minimal/README.md):

  • 它替代了手工//go:wasmexport导出函数的方式,由 PDK 的metadata包自动生成所有nd_*导出;
  • metadata包中可实现的 provider 接口是增量式的:ArtistMBIDProvider(MusicBrainz ID)、ArtistURLProvider(外部 URL)、ArtistBiographyProvider(传记)、SimilarArtistsProvider(相似艺人)、ArtistImagesProvider(艺人图片)、ArtistTopSongsProvider(热门单曲)、AlbumInfoProvider(专辑信息)、AlbumImagesProvider(专辑图片)等,只实现你数据源支持的即可,其余方法不必实现。

向 minimal 扩展更多能力时,只需让minimalPlugin实现更多 provider 接口,例如:

  • ArtistMBIDProvider- 获取艺人 MusicBrainz ID
  • ArtistURLProvider- 获取艺人外部 URL
  • SimilarArtistsProvider- 获取相似艺人
  • ArtistImagesProvider- 获取艺人图片
  • ArtistTopSongsProvider- 获取艺人热门单曲
  • AlbumInfoProvider- 获取专辑信息
  • AlbumImagesProvider- 获取专辑图片

方案二:用 XTP CLI 脚手架生成

从能力 schema 生成样板代码,适合从规范出发、按模板工程的思路开发:

# 安装 XTP CLI 后执行 xtp plugin init \ --schema-file ../capabilities/metadata_agent.yaml \ --template go \ --path ./my-plugin \ --name my-plugin # 然后创建 manifest.json 并打包 cd my-plugin xtp plugin build zip -j my-plugin.ndp manifest.json dist/plugin.wasm

(在仓库根目录视角下,schema 文件应写作plugins/capabilities/metadata_agent.yaml。)plugins/capabilities/目录下提供了官方的能力 schema:

  • metadata_agent.yaml– 艺人/专辑元数据
  • scrobbler.yaml– 刮擦集成
  • lifecycle.yaml– 初始化回调
  • scheduler_callback.yaml– 定时任务
  • websocket_callback.yaml– WebSocket 事件

方案三:使用其他语言

参考语言专属示例:Python 看 coverartarchive-py,Rust 看 webhook-rs。三种语言的实际写法差异将在下一节深入拆解。

示例深度拆解:从"能跑"到"会写"

Minimal(Go):最简骨架

演示要点:manifest 导出、单一能力函数、基础输入输出处理。如上节所示,核心是"空main()+init()注册 + 编译期接口断言 + 实现 provider 方法"四件套。注意func main() {}是 Wasm 插件必需的占位入口。

Wikimedia(Go):真实世界的元数据代理

这是最值得精读的 Go 示例,它把元数据代理的实战要素全部串了起来(见 plugins/examples/wikimedia/main.go):

  • 对外部 API 发 HTTP 请求:通过宿主服务host.HTTPSend调用,而不是直接使用 Go 标准库或 Extism 自带 HTTP(Navidrome 禁用了 Extism 内置 HTTP,host.HTTPSend是唯一受支持的方式)。请求封装为host.HTTPRequest{Method, URL, Headers, Body, TimeoutMs},例如sparqlQuery函数向 Wikidata 发送 POST 请求并声明Accept: application/sparql-results+json(main.go#L77-L109)。
  • SPARQL 查询(Wikidata):构造SELECT ?sitelink WHERE { ?artist wdt:P434 "<mbid>" ... }之类的查询,优先用 MBID(wdt:P434属性)定位,其次回退到rdfs:label名称匹配;同时还会向 DBpedia 的 SPARQL 端点查询,并调用 MediaWiki API(en.wikipedia.org/w/api.php)获取页面摘要作为艺人传记。
  • 错误处理与降级链:GetArtistURL的查找顺序是 Wikidata → DBpedia → 维基百科搜索 URL 兜底(main.go#L251-L282);GetArtistBiography则是先定位 Wikipedia URL,取页面摘要失败后再回退 DBpedia 的rdfs:comment短简介(main.go#L285-L334)。配合pdk.Log输出 Debug/Info 级别的日志辅助排查。
  • 主机白名单(Host Allowlisting):在 manifest.json 中声明permissions.http.requiredHosts为query.wikidata.org、dbpedia.org、en.wikipedia.org。沙箱只放行这些域名;本地 CLI 测试时则用--allow-host等价放行。这也是为什么该插件需要三个数据源域名都列入白名单——任一遗漏都会导致请求被沙箱拦截。

Crypto Ticker(Go):多能力的实时数据演示

演示了 Lifecycle(nd_on_init,插件加载完成后初始化连接)、SchedulerCallback(心跳与超时管理)与 WebSocketCallback(维持与行情服务的实时长连接)的组合用法,是理解"多能力并存"的最小完整案例:初始化时建立 WebSocket,定时器驱动心跳与断线重连,推送行情更新。

Cover Art Archive(Python):Python 元数据代理

演示 extism-py 插件的结构:Python 源码位于plugin/__init__.py,通过@extism.plugin_fn导出nd_*能力函数,通过@extism.import_fn("extism:host/user", ...)导入宿主函数,发起 HTTP 请求、处理 JSON 响应,最后打包为 wasm。它同时示范了 Python 插件与 Go 插件在宿主函数导入方式上的根本差异(Go 用 PDK 封装,Python 需手工声明导入并自行处理内存偏移与 JSON 编解码)。

Now Playing Logger(Python):Scheduler + SubsonicAPI

这个示例(见 plugins/examples/nowplaying-py/plugin/init.py)值得单独精读,因为它展示了 Python 侧调用宿主服务的完整样板:

  • 导入宿主函数:@extism.import_fn("extism:host/user", "scheduler_schedulerecurring")与@extism.import_fn("extism:host/user", "subsonicapi_call")(init.py#L28-L37),说明宿主服务统一挂在extism:host/user命名空间下。
  • 封装层:手写 wrapper 完成"请求 JSON →extism.memory.alloc分配内存 → 传入 offset → 读取返回 offset →extism.memory.string取回 JSON → 检查error字段"的标准调用链(init.py#L47-L96)。
  • Lifecycle 中注册定时任务:nd_on_init中读取配置项cron(默认*/1 * * * *每分钟),调用scheduler_schedule_recurring注册循环任务,scheduleId固定为"nowplaying-check"(init.py#L104-L120)。
  • SchedulerCallback 中消费事件:nd_scheduler_callback里校验scheduleId后,以配置项user(默认admin)调用getNowPlaying?u=<user>子sonic API,解析subsonic-response.nowPlaying.entry并逐条打印"谁在听什么歌"(init.py#L123-L168)。注意subsonicapi_call是进程内调用Subsonic API,无网络往返。

Webhook(Rust):Rust Scrobbler

展示 Rust 插件的完整结构(见 plugins/examples/webhook-rs/src/lib.rs):

  • 宏注册导出:nd_pdk::register_scrobbler!(WebhookPlugin)一行生成全部 Scrobbler WASM 导出(lib.rs#L23)。
  • 实现Scrobblertrait:必须实现is_authorized、now_playing、scrobble、playback_report四个方法(前文提到 Scrobbler 四个方法均必选)。
  • 极简依赖:仅依赖extism-pdk与nd-pdk,宿主 HTTP 通过nd_pdk::host::http::send调用;配置通过extism_pdk::config::get("urls")读取逗号分隔的 webhook 地址列表,scrobble 事件到达时对每个 URL 发起带查询参数的 GET 请求(lib.rs#L59-L115)。配置示例:
[PluginConfig.webhook-rs] urls = "https://example.com/webhook1,https://example.com/webhook2"

Library Inspector(Rust):Library + Scheduler 组合

在nd_on_init中注册周期任务,nd_scheduler_callback触发时通过library::get_all_libraries()宿主服务读取全部音乐库的统计信息(歌曲数、专辑数、艺人数、总大小、总时长等)并输出日志,展示"定时轮询 + 库元数据只读访问"这一典型运维型插件模式。

Discord Rich Presence(Rust):最复杂的综合案例

官方把它当作"多能力插件"的标杆,覆盖了:

  • Scrobbler– 接收播放事件
  • WebSocket– 维持与 Discord Gateway 的长连接
  • Scheduler– 心跳与超时管理
  • Cache– 连接状态存储(进程内 TTL 缓存)
  • Artwork– 获取专辑封面 URL 用于展示

它同时使用 HTTP、WebSocket、Cache、Scheduler、Artwork、Config 六个宿主服务,是理解"插件如何与外部实时服务集成"的最佳全景参考。注意其 manifest 中 WebSocket 权限的requiredHosts是必填的(如*.discord.gg),这与 HTTP 权限(未声明requiredHosts时仅允许公网地址)的行为不同。

深入学习资源

  • 插件系统完整文档:能力函数签名表(MetadataAgent 的 11 个函数、Scrobbler、Lyrics、SonicSimilarity、TaskWorker、Lifecycle、SchedulerCallback、WebSocketCallback)、全部宿主服务(HTTP/Scheduler/Cache/KVStore/Storage/Task/WebSocket/Library/Matcher/Artwork/SubsonicAPI/Config/Users/ScrobbleRetriever)的参数与 Go/Rust 用法示例、navidrome plugin命令行管理工具、安全模型(主机白名单、受限文件系统、禁止监听端口、配置隔离、用户范围授权)。
  • plugins/capabilities/:能力 schema(YAML),供 XTP CLI 脚手架与参考实现使用。
  • plugins/pdk/:官方 PDK 源码(Go、Rust、Python、JS),其中 Go 侧提供metadata、scrobbler、lyrics、sonicsimilarity、taskworker、lifecycle、scheduler、websocket、host、types、pdk等类型安全包,是编写 Go 插件时最重要的"标准库"。
  • 各示例目录内的 README 与源码:minimal/README.md、wikimedia/README.md、crypto-ticker/README.md、coverartarchive-py/README.md、nowplaying-py/README.md、webhook-rs/README.md、library-inspector-rs/README.md、discord-rich-presence-rs/README.md,每个都包含该示例独有的构建与配置说明。

小结:一条完整的插件开发链路

综合官方示例,一条完整的插件开发链路是:选语言(Go 体验最佳 / Rust 性能最优 / Python 原型最快)→选起点(复制 minimal 或xtp plugin init脚手架)→声明 manifest(name、author、version必填,权限按需声明并遵循"最小权限"原则)→实现能力函数(Go 用metadata.Register()等 PDK 注册模式,Rust 用register_scrobbler!宏,Python 用@extism.plugin_fn)→按需调用宿主服务(host.HTTPSend、scheduler_schedule_recurring、subsonicapi_call等)→构建打包(tinygo build+zip -j x.ndp manifest.json plugin.wasm,或用make系列目标)→先测后装(Extism CLI 带--allow-host单测,再复制.ndp到插件目录并在navidrome.toml启用)。按这条链路,从plugins/examples/出发,你可以在数小时内产出第一个可运行的 Navidrome 插件。

  • 后端
  • 音视频
  • 前端

【免费下载链接】navidrome

🎧 Your Personal Streaming Service

项目地址:https://gitcode.com/gh_mirrors/na/navidrome
点击查看免费下载

相关推荐

上一篇:xcit_tiny_12_p8_384.fb_dist_in1k模型蒸馏技术详解:知识蒸馏在图像分类中的应用
下一篇:RVC 低资源语音转换终极指南:10 分钟录音训练 AI 语音克隆,手把手跑通全流程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询