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

  1. 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
  1. Add Soulsolid as a dependency:
go get github.com/contre95/soulsolid/src/music
go get github.com/contre95/soulsolid/src/features/downloading
  1. 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
}
  1. 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: 30

Distribution

There are two ways to distribute plugins:

Option 1: Pre-built .so files

  1. Build for the target platform: Make sure to build the plugin for the same OS and architecture as the Soulsolid binary.
  2. Distribute the .so file: Users can place the .so file anywhere accessible to Soulsolid and configure the path in their config.

Option 2: Git repositories

  1. Publish your plugin source code in a public git repository with a valid go.mod file.
  2. 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

  1. Soulsolid clones the repository (default branch only, depth=1)
  2. Adds a replace directive to use the local soulsolid module
  3. Runs go mod tidy to resolve dependencies
  4. Builds the plugin with go build -buildmode=plugin
  5. Loads the built .so file 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.mod file at the root
  • Building arbitrary code carries inherent security risks (same as loading pre-built .so files)

Best Practices

  • Error handling: Return meaningful errors from all methods.
  • Logging: Use the standard log/slog package 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:

  1. Building it with go build -buildmode=plugin
  2. Adding it to your Soulsolid config
  3. Restarting Soulsolid
  4. 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.