LibreLyrics LibreLyrics

Plugin Development

LibreLyrics is plugin-first — every lyrics provider is a self-contained module. This guide shows how to build, configure, and distribute your own.

How Plugins Work

  1. On startup, LibreLyrics scans for installed packages that declare the librelyrics.plugins entry-point.
  2. Each discovered class is validated against the current API version.
  3. When fetch(url) is called, the orchestrator matches the URL against every plugin's url_regex and dispatches to the first match.

Minimal Plugin

Subclass LyricsModule, implement meta() and fetch().

from librelyrics.modules.base import LyricsModule, ModuleMeta
from librelyrics.models import (
    LyricsResponse, LyricsLine, LyricsType, ModuleCapability
)


class AcmeModule(LyricsModule):
    """Lyrics provider for acme-lyrics.example."""

    @staticmethod
    def meta() -> ModuleMeta:
        return ModuleMeta(
            name="Acme Lyrics",
            version="0.1.0",
            url_regex=r"https?://acme-lyrics\.example/track/(\w+)",
            api_version=1,
            capabilities=frozenset({ModuleCapability.SINGLE_TRACK}),
            lyrics_types=frozenset({LyricsType.SYNCED}),
        )

    def fetch(self, url: str) -> LyricsResponse:
        track_id = self._extract_id(url)
        data = self._api_get(f"/v1/track/{track_id}/lyrics")
        lines = [
            LyricsLine(text=l["text"], start_ms=l["start"])
            for l in data["lyrics"]
        ]
        return LyricsResponse(
            title=data["title"],
            artist=data["artist"],
            lyrics=lines,
            source=self.meta().name,
            synced=True,
        )

    def _extract_id(self, url):
        import re
        return re.search(self.meta().url_regex, url).group(1)

    def _api_get(self, endpoint):
        import requests
        resp = requests.get(f"https://api.acme-lyrics.example{endpoint}")
        resp.raise_for_status()
        return resp.json()

ModuleMeta Reference

ModuleMeta(
    name:         str,            # Human-readable plugin name
    version:      str,            # SemVer plugin version
    url_regex:    str,            # Regex matching supported track URLs
    api_version:  int,            # Must equal CURRENT_API_VERSION (1)
    capabilities: frozenset[ModuleCapability],
    lyrics_types: frozenset[LyricsType],
)
Important: If api_version doesn't match the host's CURRENT_API_VERSION, the plugin is rejected with PluginAPIVersionError.

Batch Capabilities (Album / Playlist)

To support albums or playlists, implement the corresponding method and add the capability flag:

1. Add the method

def fetch_album(self, url: str) -> list[LyricsResponse]:
    album = self._api_get(f"/v1/album/{self._extract_id(url)}")
    results = []
    for track in album["tracks"]:
        try:
            results.append(self.fetch(track["url"]))
        except Exception:
            continue
    return results

2. Declare the capability

@staticmethod
def meta():
    return ModuleMeta(
        # ...
        capabilities=frozenset({
            ModuleCapability.SINGLE_TRACK,
            ModuleCapability.ALBUM,
        }),
    )

Plugin Configuration

Define defaults via default_config(). User overrides are merged automatically and available via self.config:

Declare defaults

@staticmethod
def default_config() -> dict:
    return {
        "sp_dc": "",
        "market": "US",
    }

Use in fetch

def fetch(self, url):
    cfg = self.config          # dict (merged defaults + user overrides)
    sp_dc = cfg["sp_dc"]
    market = cfg.get("market", "US")
    # ...

Lifecycle Hooks

Override setup() / teardown() for one-time initialisation and cleanup:

class AcmeModule(LyricsModule):
    def setup(self):
        """Called once when plugin is loaded."""
        self.session = requests.Session()
        self.session.headers.update({"X-Api-Key": self.config["api_key"]})

    def teardown(self):
        """Called when LibreLyrics shuts down."""
        self.session.close()

Retry & Back-off

LibreLyrics automatically retries on RateLimitError and transient network failures using exponential back-off. You can override the defaults per-plugin:

@staticmethod
def default_config():
    return {
        "retry_count": 5,       # override for this plugin
        "retry_delay": 3,       # longer initial wait
    }

Packaging & Distribution

Plugins are standard Python packages. Register the entry-point in your pyproject.toml:

Entry Point

# pyproject.toml (your plugin's)
[project.entry-points."librelyrics.plugins"]
acme = "acme_lyrics.plugin:AcmeModule"

Install & test

pip install acme-lyrics-plugin
# LibreLyrics will auto-discover it on next run

Verify

from librelyrics import LibreLyrics

ll = LibreLyrics()
for plugin in ll.list_plugins():
    print(plugin.meta().name, plugin.meta().version)