本指南帮助第三方开发者为 MusicBot-Go 开发新的音乐平台插件。
如果你希望编写动态脚本插件,请参考 plugins/scripts/README.md。
MusicBot-Go 使用基于接口的插件系统,允许轻松扩展对不同音乐平台的支持。
- Platform 接口: 定义音乐平台的核心功能(下载、搜索、歌词等)。
- Registry: 管理已注册的平台插件。
- Manager: 提供高级 API 供 Bot 使用,负责路由请求到正确的平台。
- Handlers: Bot 处理程序通过 Manager 自动与各个平台交互。
插件采用能力导向设计,开发者可以选择性实现功能:
SupportsDownload()- 是否支持下载SupportsSearch()- 是否支持搜索SupportsLyrics()- 是否支持歌词SupportsRecognition()- 是否支持识曲
对于不支持的功能,方法应返回 platform.ErrUnsupported。
主项目现在提供了通用插件设置接口,插件可以独立定义自己的设置项,不需要在主项目里为每个插件单独加数据库字段。
- 插件在
register.go通过Contribution.SettingDefinitions注册设置定义。 - 设置值统一存储在数据库
plugin_settings表(按作用域 user/group 隔离)。 /settings面板会自动渲染这些设置项。
可用类型定义见:bot/plugin_settings.go
PluginSettingDefinitionPluginSettingOptionPluginScopeUser/PluginScopeGroup
仓储接口见:bot/interfaces.go
GetPluginSetting(...)SetPluginSetting(...)
设计建议:插件“行为开关/模式”优先走插件设置,不要再向
UserSettings/GroupSettings增加平台专用字段。
- Go 1.26.0+
- 熟悉目标音乐平台的 API
- 了解 Go 接口和错误处理
- 创建包目录:
plugins/<platform_name>/ - 实现
Platform接口: 在该目录下创建platform.go。 - 实现
URLMatcher/TextMatcher接口 (可选但推荐): 允许 Bot 识别该平台的 URL 或短链/纯 ID 文本。 - 编写测试: 确保插件逻辑正确。
- 注册插件: 在插件包内通过工厂注册,并在
plugins/all中进行空白导入。
package examplemusic
import (
"context"
"io"
"github.com/liuran001/MusicBot-Go/bot/platform"
)
type ExampleMusicPlatform struct{}
func (p *ExampleMusicPlatform) Name() string {
return "examplemusic"
}
func (p *ExampleMusicPlatform) SupportsDownload() bool { return false }
func (p *ExampleMusicPlatform) SupportsSearch() bool { return false }
func (p *ExampleMusicPlatform) SupportsLyrics() bool { return false }
func (p *ExampleMusicPlatform) SupportsRecognition() bool { return false }
func (p *ExampleMusicPlatform) Capabilities() platform.Capabilities {
return platform.Capabilities{}
}
func (p *ExampleMusicPlatform) GetDownloadInfo(ctx context.Context, trackID string, quality platform.Quality) (*platform.DownloadInfo, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) Search(ctx context.Context, query string, limit int) ([]platform.Track, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) GetLyrics(ctx context.Context, trackID string) (*platform.Lyrics, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) RecognizeAudio(ctx context.Context, audioData io.Reader) (*platform.Track, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) GetTrack(ctx context.Context, trackID string) (*platform.Track, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) GetArtist(ctx context.Context, artistID string) (*platform.Artist, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) GetAlbum(ctx context.Context, albumID string) (*platform.Album, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) GetPlaylist(ctx context.Context, playlistID string) (*platform.Playlist, error) {
return nil, platform.ErrUnsupported
}
// 其他方法按需实现或返回 ErrUnsupported完整定义见 bot/platform/interface.go。
Name() string
- 返回平台唯一标识符 (小写,如 "netease", "qqmusic")。
- 用于 URL 路由、缓存键和日志。
能力检查方法
SupportsDownload() boolSupportsSearch() boolSupportsLyrics() boolSupportsRecognition() boolCapabilities() Capabilities
*GetDownloadInfo(ctx context.Context, trackID string, quality Quality) (DownloadInfo, error)
- 获取下载信息(URL、大小、格式、码率等)。
trackID: 平台特定的曲目 ID。quality: 请求的音质 (standard/high/lossless/hires)。- 注意: 即使不支持请求的音质,也应返回最佳可用音质。
Search(ctx context.Context, query string, limit int) ([]Track, error)
- 搜索曲目。
- 返回:
Track切片,最多limit个结果。
*GetLyrics(ctx context.Context, trackID string) (Lyrics, error)
- 获取歌词。
- 返回:
Lyrics结构(支持纯文本和带时间戳的歌词)。
*GetTrack(ctx context.Context, trackID string) (Track, error)
- 获取曲目详情(标题、艺术家、专辑封面等)。
GetArtist/GetAlbum/GetPlaylist
- 获取艺术家/专辑/歌单详情。
- 如暂不支持,返回
platform.ErrUnsupported。
*RecognizeAudio(ctx context.Context, audioData io.Reader) (Track, error)
- 听歌识曲。
- 接收原始音频流,返回识别到的曲目。
URLMatcher: 解析平台 URL。TextMatcher: 解析短链/纯 ID 文本(例如分享短链)。AutoParseDecider: 插件自定义“是否允许自动解析”。
AutoParseDecider 定义见 bot/platform/interface.go:
type AutoParseDecider interface {
AutoParseSettingKey() string
ShouldAutoParse(ctx context.Context, trackID string, mode string) (bool, error)
}典型用法:
- 在插件里定义设置项(如
parse_mode=on/off/...)并注册。 - 在平台实现
AutoParseDecider,根据mode+ 平台元数据判定是否自动解析。 - 主项目会在自动解析链路里统一调用该接口。
以下用虚构的 examplemusic 平台演示一个完整插件。真实插件可参考 plugins/kuwo/(多档位校验)
或 plugins/soda/(结构最简)。
plugins/examplemusic/
├── platform.go # 主实现
├── matcher.go # URL 匹配
├── types.go # 类型转换辅助
├── register.go # 插件注册
└── platform_test.go
package examplemusic
import (
"context"
"io"
"fmt"
"github.com/liuran001/MusicBot-Go/bot/platform"
)
type ExampleMusicPlatform struct {
client *Client
}
func New(client *Client) *ExampleMusicPlatform {
return &ExampleMusicPlatform{client: client}
}
func (p *ExampleMusicPlatform) Name() string {
return "examplemusic"
}
func (p *ExampleMusicPlatform) SupportsDownload() bool {
return false // 该平台只提供元数据,不支持直接下载音频流
}
func (p *ExampleMusicPlatform) SupportsSearch() bool {
return true
}
func (p *ExampleMusicPlatform) SupportsLyrics() bool {
return false
}
func (p *ExampleMusicPlatform) SupportsRecognition() bool {
return false
}
func (p *ExampleMusicPlatform) Capabilities() platform.Capabilities {
return platform.Capabilities{Search: true}
}
func (p *ExampleMusicPlatform) GetDownloadInfo(ctx context.Context, trackID string, quality platform.Quality) (*platform.DownloadInfo, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) Search(ctx context.Context, query string, limit int) ([]platform.Track, error) {
results, err := p.client.SearchTracks(ctx, query, limit)
if err != nil {
return nil, fmt.Errorf("examplemusic search: %w", err)
}
var tracks []platform.Track
for i := range results {
tracks = append(tracks, p.convertTrack(&results[i]))
}
return tracks, nil
}
func (p *ExampleMusicPlatform) GetTrack(ctx context.Context, trackID string) (*platform.Track, error) {
track, err := p.client.GetTrack(ctx, trackID)
if err != nil {
return nil, platform.NewNotFoundError("examplemusic", "track", trackID)
}
res := p.convertTrack(track)
return &res, nil
}
func (p *ExampleMusicPlatform) GetLyrics(ctx context.Context, trackID string) (*platform.Lyrics, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) RecognizeAudio(ctx context.Context, audioData io.Reader) (*platform.Track, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) GetArtist(ctx context.Context, artistID string) (*platform.Artist, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) GetAlbum(ctx context.Context, albumID string) (*platform.Album, error) {
return nil, platform.ErrUnsupported
}
func (p *ExampleMusicPlatform) GetPlaylist(ctx context.Context, playlistID string) (*platform.Playlist, error) {
return nil, platform.ErrUnsupported
}
// 其他方法实现...func (p *ExampleMusicPlatform) convertTrack(st *apiTrack) platform.Track {
artists := make([]platform.Artist, len(st.Artists))
for i, a := range st.Artists {
artists[i] = platform.Artist{
ID: a.ID,
Name: a.Name,
Platform: "examplemusic",
}
}
return platform.Track{
ID: st.ID,
Platform: "examplemusic",
Title: st.Name,
Artists: artists,
Duration: time.Duration(st.DurationMS) * time.Millisecond,
CoverURL: st.CoverURL,
}
}实现 URLMatcher 接口允许 Bot 自动识别并处理特定平台的链接;实现 TextMatcher 可处理短链/纯 ID 等文本输入。
package examplemusic
import (
"net/url"
"strings"
)
type URLMatcher struct{}
func (m *URLMatcher) MatchURL(rawURL string) (string, bool) {
u, err := url.Parse(rawURL)
if err != nil {
return "", false
}
// 匹配 music.example.com/track/xxx
if !strings.Contains(u.Host, "music.example.com") {
return "", false
}
parts := strings.Split(strings.TrimPrefix(u.Path, "/"), "/")
if len(parts) >= 2 && parts[0] == "track" {
return parts[1], true
}
return "", false
}func (p *ExampleMusicPlatform) MatchText(text string) (string, bool) {
// 解析短链或纯 ID,返回 trackID
return "", false
}// 在 platform.go 中
func (p *ExampleMusicPlatform) MatchURL(url string) (string, bool) {
return (&URLMatcher{}).MatchURL(url)
}请使用 bot/platform/errors.go 中定义的统一错误处理机制。
- 资源未找到:
platform.NewNotFoundError(platform, resource, id) - 速率限制:
platform.NewRateLimitedError(platform) - 内容不可用:
platform.NewUnavailableError(platform, resource, id) - 功能不支持:
platform.NewUnsupportedError(platform, feature)
示例:
if err == api.ErrNotFound {
return nil, platform.NewNotFoundError("myplatform", "track", trackID)
}建议为插件编写单元测试,特别是 URL 匹配和类型转换逻辑。
func TestURLMatcher(t *testing.T) {
matcher := &URLMatcher{}
tests := []struct {
url string
wantID string
ok bool
}{
{"https://music.example.com/track/abc123", "abc123", true},
{"https://music.163.com/song?id=123", "", false},
}
for _, tt := range tests {
id, ok := matcher.MatchURL(tt.url)
if ok != tt.ok || id != tt.wantID {
t.Errorf("MatchURL(%s) = (%s, %v), want (%s, %v)", tt.url, id, ok, tt.wantID, tt.ok)
}
}
}在插件包内注册工厂,并在 plugins/all 中添加空白导入。示例:
// plugins/examplemusic/register.go
package examplemusic
import (
"github.com/liuran001/MusicBot-Go/bot/config"
logpkg "github.com/liuran001/MusicBot-Go/bot/logger"
platformplugins "github.com/liuran001/MusicBot-Go/bot/platform/plugins"
)
func init() {
if err := platformplugins.Register("examplemusic", buildContribution); err != nil {
panic(err)
}
}
func buildContribution(cfg *config.Config, logger *logpkg.Logger) (*platformplugins.Contribution, error) {
client := NewClient(cfg.GetPluginString("examplemusic", "api_key"))
platform := NewPlatform(client)
return &platformplugins.Contribution{Platform: platform}, nil
}
Contribution还可选提供ID3标签提供器与Recognizer识曲服务。
// plugins/all/all.go
package all
import (
_ "github.com/liuran001/MusicBot-Go/plugins/examplemusic"
)[plugins.<name>] 段是自动解析的,不需要改动 bot/config/。在 config_example.ini
里补上带注释的配置项,插件用 cfg.GetPluginString/GetPluginInt/GetPluginBool( "<name>", "<key>") 读取即可。约定项:enabled(是否启用)、timeout(秒)、
api_proxy_*(平台级 API 代理,用 cfg.ResolveAPIProxyConfig("<name>") 取)。
需要在运行时回写配置(如 Cookie 自动续期)时用 cfg.PersistPluginConfig("<name>", pairs)。
- 并发安全:
Platform实例会被多个 goroutine 并发调用,请确保实现是线程安全的。 - Context 尊重: 始终将
context.Context传递给底层网络请求,并尊重其取消信号。 - 音质映射: 将平台特有的音质定义映射到
platform.Quality枚举。 - 日志记录: 使用项目统一的日志组件记录关键操作和非预期错误。
- 优雅降级: 如果某个功能(如歌词)获取失败,不应影响主流程,应返回清晰的错误。
Q: 我的平台不支持下载,只能搜索,可以吗?
A: 完全可以。只需在 SupportsDownload() 返回 false,并在 GetDownloadInfo() 中返回 platform.ErrUnsupported。
Q: 如何处理 API Token 过期? A: 建议在插件内部实现 Token 自动刷新机制,对外部调用者透明。
Q: 插件需要依赖外部二进制文件(如 ffmpeg)怎么办? A: 请在文档中注明,并在插件初始化时检查依赖是否存在。