Plugins
This guide is for developers who want to write a downloader plugin for Soulsolid in Go. If you just want to use an existing plugin, see Downloading instead; no code needed.
Overview
Soulsolid supports pluggable downloaders that can be loaded from .so files at runtime. This allows developers to create their own downloaders in separate repositories and distribute them independently.
Plugin Interface
Your plugin must implement the Downloader interface:
type Downloader interface {
// Search methods
SearchAlbums(query string, limit int) ([]music.Album, error)
SearchTracks(query string, limit int) ([]music.Track, error)
SearchArtists(query string, limit int) ([]music.Artist, error)
SearchLinks(query string, limit int) (*LinkResult, error)
// Navigation methods
GetAlbumTracks(albumID string) ([]music.Track, error)
GetArtistAlbums(artistID string) ([]music.Album, error)
GetChartTracks(limit int) ([]music.Track, error)
// Download methods
DownloadTrack(trackID string, downloadDir string, progressCallback func(downloaded, total int64)) (*music.Track, error)
DownloadAlbum(albumID string, downloadDir string, progressCallback func(downloaded, total int64)) ([]*music.Track, error)
DownloadArtist(artistID string, downloadDir string, progressCallback func(downloaded, total int64)) ([]*music.Track, error)
DownloadLink(url string, downloadDir string, progressCallback func(downloaded, total int64)) ([]*music.Track, error)
// User info
GetUserInfo() *UserInfo
GetStatus() DownloaderStatus
Name() string
Capabilities() DownloaderCapabilities
}DownloaderCapabilities tells Soulsolid which UI features to expose for your plugin:
type DownloaderCapabilities struct {
SupportsSearch bool // track/album search
SupportsArtistSearch bool // artist search
SupportsDirectLinks bool // URL/link input
SupportsChartTracks bool // charts section
}Return downloading.ErrMethodNotSupported from any method your plugin does not implement.
Creating a Plugin
- Create a new Go module for your plugin:
mkdir my-downloader-plugin
cd my-downloader-plugin
go mod init github.com/yourusername/my-downloader-plugin- Add Soulsolid as a dependency:
go get github.com/contre95/soulsolid/src/music
go get github.com/contre95/soulsolid/src/features/downloading- Implement your downloader:
package main
import (
"github.com/contre95/soulsolid/src/features/downloading"
"github.com/contre95/soulsolid/src/music"
)
type MyDownloader struct {
// Your configuration fields
apiKey string
baseURL string
}
// Implement the Downloader interface
func (d *MyDownloader) Name() string {
return "MyDownloader"
}
func (d *MyDownloader) SearchAlbums(query string, limit int) ([]music.Album, error) {
// Implement album search
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) SearchTracks(query string, limit int) ([]music.Track, error) {
// Implement track search
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) SearchArtists(query string, limit int) ([]music.Artist, error) {
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) SearchLinks(query string, limit int) (*downloading.LinkResult, error) {
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) GetAlbumTracks(albumID string) ([]music.Track, error) {
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) GetArtistAlbums(artistID string) ([]music.Album, error) {
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) GetChartTracks(limit int) ([]music.Track, error) {
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) DownloadTrack(trackID string, downloadDir string, progressCallback func(downloaded, total int64)) (*music.Track, error) {
// Implement track download
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) DownloadAlbum(albumID string, downloadDir string, progressCallback func(downloaded, total int64)) ([]*music.Track, error) {
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) DownloadArtist(artistID string, downloadDir string, progressCallback func(downloaded, total int64)) ([]*music.Track, error) {
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) DownloadLink(url string, downloadDir string, progressCallback func(downloaded, total int64)) ([]*music.Track, error) {
return nil, downloading.ErrMethodNotSupported
}
func (d *MyDownloader) GetUserInfo() *downloading.UserInfo {
return &downloading.UserInfo{Name: "MyDownloader User"}
}
func (d *MyDownloader) GetStatus() downloading.DownloaderStatus {
return downloading.DownloaderStatus{Connected: true}
}
func (d *MyDownloader) Capabilities() downloading.DownloaderCapabilities {
return downloading.DownloaderCapabilities{
SupportsSearch: true,
}
}
// Export the NewDownloader function (this is required!)
func NewDownloader(config map[string]interface{}) (downloading.Downloader, error) {
apiKey, ok := config["api_key"].(string)
if !ok {
return nil, fmt.Errorf("api_key is required")
}
baseURL, ok := config["base_url"].(string)
if !ok {
baseURL = "https://api.example.com" // default
}
return &MyDownloader{
apiKey: apiKey,
baseURL: baseURL,
}, nil
}- Build the plugin:
go build -buildmode=plugin -o mydownloader.so .Configuration
Plugins receive their configuration through the NewDownloader function as a map[string]interface{}. The configuration comes from the Soulsolid config file.
Each plugin can optionally specify an icon path for display in the UI.
Example config.yaml:
downloaders:
plugins:
- name: "mydownloader"
# Either specify a URL to build from git:
url: "https://github.com/user/soulsolid-myplugin.git"
# Or specify a path to a pre-built .so file:
# path: "/path/to/mydownloader.so"
icon: "path/to/icon.png" # Optional icon for the downloader
config:
api_key: "your_api_key_here"
base_url: "https://api.example.com"
timeout: 30Distribution
There are two ways to distribute plugins:
Option 1: Pre-built .so files
- Build for the target platform: Make sure to build the plugin for the same OS and architecture as the Soulsolid binary.
- Distribute the .so file: Users can place the
.sofile anywhere accessible to Soulsolid and configure the path in their config.
Option 2: Git repositories
- Publish your plugin source code in a public git repository with a valid
go.modfile. - Users configure the repository URL in their Soulsolid config (see “Building from Git Repositories” section below).
Version compatibility
Plugins should be built against the same version of Soulsolid to ensure API compatibility.
Building from Git Repositories
Soulsolid can automatically build plugins directly from git repositories. Instead of distributing pre-built .so files, you can specify a repository URL in your configuration.
Configuration Example
downloaders:
plugins:
- name: "mydownloader"
url: "https://github.com/user/soulsolid-myplugin.git"
# path: "/fallback.so" # Optional fallback path if URL building fails
config:
api_key: "your_api_key_here"Requirements
- git must be installed and available in PATH
- go must be installed (same version as the main application) and available in PATH
- Network access to clone the repository
How It Works
- Soulsolid clones the repository (default branch only, depth=1)
- Adds a replace directive to use the local soulsolid module
- Runs
go mod tidyto resolve dependencies - Builds the plugin with
go build -buildmode=plugin - Loads the built
.sofile into memory
Notes
- Plugins are rebuilt each time Soulsolid starts (no caching)
- Only the default branch is cloned (no branch/tag/commit support)
- Repository must contain a
go.modfile at the root - Building arbitrary code carries inherent security risks (same as loading pre-built
.sofiles)
Best Practices
- Error handling: Return meaningful errors from all methods.
- Logging: Use the standard
log/slogpackage for logging. - Configuration validation: Validate required configuration in
NewDownloader. - Progress callbacks: Implement progress callbacks for downloads to provide user feedback.
- Status reporting: Return appropriate status information in
GetStatus(). - Thread safety: Ensure your downloader is safe for concurrent use.
Example Plugin
There’s a dummy plugin I have created to serve as an example. You can find it here.
Testing
Test your plugin by:
- Building it with
go build -buildmode=plugin - Adding it to your Soulsolid config
- Restarting Soulsolid
- Testing the downloader through the web interface
Troubleshooting
- Plugin not loading: Check that the path is correct and the file is readable.
- Symbol not found: Ensure you export
NewDownloader(capital N). - Configuration errors: Check that required config keys are provided.
- Version mismatches: Rebuild the plugin when updating Soulsolid.