- 后端
- 音视频
- 前端
【免费下载链接】navidrome
🎧 Your Personal Streaming Service
导读
本指南以 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) | 核心演示点 |
|---|---|---|---|
| minimal | Go | MetadataAgent | 最基础的插件结构与注册模式 |
| wikimedia | Go | MetadataAgent | Wikidata/Wikipedia 元数据抓取,真实世界级实现 |
| crypto-ticker | Go | Lifecycle、SchedulerCallback、WebSocketCallback | 实时加密货币行情(演示类) |
| coverartarchive-py | Python | MetadataAgent | Cover Art Archive 封面抓取 |
| nowplaying-py | Python | Lifecycle、SchedulerCallback | 定时记录当前播放(Now Playing 日志) |
| webhook-rs | Rust | Scrobbler | 播放 scrobble 时触发 HTTP Webhook |
| library-inspector-rs | Rust | Lifecycle、SchedulerCallback | 定时输出音乐库统计信息 |
| discord-rich-presence-rs | Rust | Scrobbler、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.ndpMakefile 的设计有几个值得留意的工程细节(见 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 内联机测试
- 将
.ndp文件复制到你的插件目录(默认<data-folder>/plugins/); - 在
navidrome.toml中启用插件:
[Plugins] Enabled = true Folder = "/path/to/plugins"- 对元数据代理类插件,把它加入 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 IDArtistURLProvider- 获取艺人外部 URLSimilarArtistsProvider- 获取相似艺人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
相关推荐
TiKV 协处理器插件示例编写指南:从 dylib 构建到插件注册
TiKV 协处理器插件示例编写指南:从 dylib 构建到插件注册 导读 TiKV 在 v2 协处理器框架( coprocessor v2 )中提供了可插拔的插
数据库KV存储分布式数据库云原生HunterPie:为《怪物猎人:世界》打造的专业级实时监控与数据可视化增强工具
HunterPie:为《怪物猎人:世界》打造的专业级实时监控与数据可视化增强工具 你是否曾在《怪物猎人:世界》的激烈狩猎中,因为无法准确掌握怪物血量状态而错失最
后端音视频前端Penpot 插件开发实战指南:运行官方示例插件与从零构建自定义插件
Penpot 插件开发实战指南:运行官方示例插件与从零构建自定义插件 Penpot 的插件体系(Penpot Plugins)为开源设计平台提供了一个可扩展的运
前端设计系统图形学协同办公
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考