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
- On startup, LibreLyrics scans for installed packages that declare the
librelyrics.pluginsentry-point. - Each discovered class is validated against the current API version.
- When
fetch(url)is called, the orchestrator matches the URL against every plugin'surl_regexand 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 results2. 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 runVerify
from librelyrics import LibreLyrics
ll = LibreLyrics()
for plugin in ll.list_plugins():
print(plugin.meta().name, plugin.meta().version)