Add LRCLIB lyrics metadata provider (#2123)
authorJozef Kruszynski <60214390+jozefKruszynski@users.noreply.github.com>
Sun, 20 Apr 2025 14:19:45 +0000 (16:19 +0200)
committerGitHub <noreply@github.com>
Sun, 20 Apr 2025 14:19:45 +0000 (16:19 +0200)
music_assistant/providers/lrclib/__init__.py [new file with mode: 0644]
music_assistant/providers/lrclib/icon.svg [new file with mode: 0644]
music_assistant/providers/lrclib/manifest.json [new file with mode: 0644]

diff --git a/music_assistant/providers/lrclib/__init__.py b/music_assistant/providers/lrclib/__init__.py
new file mode 100644 (file)
index 0000000..3f889d4
--- /dev/null
@@ -0,0 +1,153 @@
+"""
+The LRCLIB Metadata provider for Music Assistant.
+
+Used for retrieval of synchronized lyrics.
+"""
+
+from __future__ import annotations
+
+import json
+from typing import TYPE_CHECKING, Any, cast
+
+from aiohttp import ClientResponseError
+from music_assistant_models.config_entries import ConfigEntry
+from music_assistant_models.enums import ConfigEntryType, ProviderFeature
+from music_assistant_models.media_items import MediaItemMetadata, Track
+
+from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries
+from music_assistant.models.metadata_provider import MetadataProvider
+
+if TYPE_CHECKING:
+    from music_assistant_models.config_entries import ConfigValueType, ProviderConfig
+    from music_assistant_models.provider import ProviderManifest
+
+    from music_assistant.mass import MusicAssistant
+    from music_assistant.models import ProviderInstanceType
+
+SUPPORTED_FEATURES = {
+    ProviderFeature.TRACK_METADATA,
+}
+
+CONF_API_URL = "api_url"
+DEFAULT_API_URL = "https://lrclib.net/api"
+USER_AGENT = "MusicAssistant (https://github.com/music-assistant/server)"
+
+
+async def setup(
+    mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
+) -> ProviderInstanceType:
+    """Initialize provider(instance) with given configuration."""
+    return LrclibProvider(mass, manifest, config)
+
+
+async def get_config_entries(
+    mass: MusicAssistant,
+    instance_id: str | None = None,
+    action: str | None = None,
+    values: dict[str, ConfigValueType] | None = None,
+) -> tuple[ConfigEntry, ...]:
+    """Return Config entries to setup this provider."""
+    # ruff: noqa: ARG001
+    return (
+        ConfigEntry(
+            key=CONF_API_URL,
+            type=ConfigEntryType.STRING,
+            label="API URL",
+            description="URL of the LRCLib API (including 'api' but excluding '/get')",
+            default_value=DEFAULT_API_URL,
+            required=False,
+        ),
+    )
+
+
+class LrclibProvider(MetadataProvider):
+    """LRCLIB provider for handling synchronized lyrics."""
+
+    async def handle_async_init(self) -> None:
+        """Handle async initialization of the provider."""
+        # Get the API URL from config
+        self.api_url = self.config.get_value(CONF_API_URL)
+
+        # Only use strict throttling if using the default API
+        if self.api_url == DEFAULT_API_URL:
+            self.throttler = ThrottlerManager(rate_limit=1, period=30)
+            self.logger.debug("Using default API with standard throttling (1 request per 30s)")
+        else:
+            # Less strict throttling for custom API endpoint
+            self.throttler = ThrottlerManager(rate_limit=1, period=1)
+            self.logger.debug("Using custom API endpoint: %s (throttling disabled)", self.api_url)
+
+    @property
+    def supported_features(self) -> set[ProviderFeature]:
+        """Return the features supported by this Provider."""
+        return SUPPORTED_FEATURES
+
+    @throttle_with_retries
+    async def _get_data(self, **params: Any) -> dict[str, Any] | None:
+        """Get data from LRCLib API with throttling and retries."""
+        headers = {"User-Agent": USER_AGENT}
+
+        try:
+            async with self.mass.http_session.get(
+                f"{self.api_url}/get", params=params, headers=headers
+            ) as response:
+                response.raise_for_status()
+                if response.status == 204:  # No content
+                    return None
+                return cast("dict[str, Any]", await response.json())
+        except ClientResponseError as err:
+            self.logger.debug("Error fetching data from LRCLib API (%s): %s", self.api_url, err)
+            return None
+        except json.JSONDecodeError as err:
+            self.logger.debug("Error parsing response from LRCLib API: %s", err)
+            return None
+
+    async def get_track_metadata(self, track: Track) -> MediaItemMetadata | None:
+        """Retrieve synchronized lyrics for a track."""
+        if track.metadata and track.metadata.lrc_lyrics:
+            self.logger.debug(
+                "Skipping lyrics lookup for %s: Already has synchronized lyrics", track.name
+            )
+            return None
+
+        if not track.artists:
+            self.logger.debug("Skipping lyrics lookup for %s: No artist information", track.name)
+            return None
+
+        artist_name = track.artists[0].name
+        album_name = track.album.name if track.album else "Unknown Album"
+
+        duration = track.duration or 0
+
+        if not duration:
+            self.logger.debug("Skipping lyrics lookup for %s: No duration information", track.name)
+            return None
+
+        self.logger.debug(
+            "Fetching synchronized lyrics for %s by %s (%s) on lrclib.net",
+            track.name,
+            artist_name,
+            album_name,
+        )
+
+        search_params = {
+            "track_name": track.name,
+            "artist_name": artist_name,
+            "album_name": album_name,
+            "duration": duration,
+        }
+
+        self.logger.debug("Searching synchronized lyrics with params: %s", search_params)
+
+        if data := await self._get_data(**search_params):
+            synced_lyrics = data.get("syncedLyrics")
+
+            if synced_lyrics:
+                metadata = MediaItemMetadata()
+                metadata.lrc_lyrics = synced_lyrics
+
+                self.logger.debug("Found synchronized lyrics for %s by %s", track.name, artist_name)
+                return metadata
+
+        self.logger.debug("No synchronized lyrics found for %s by %s", track.name, artist_name)
+        return None
diff --git a/music_assistant/providers/lrclib/icon.svg b/music_assistant/providers/lrclib/icon.svg
new file mode 100644 (file)
index 0000000..541b166
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   version="1.1"
+   id="svg1"
+   width="200"
+   height="200"
+   viewBox="0 0 200 200"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <g
+     id="g1">
+    <image
+       width="200"
+       height="200"
+       preserveAspectRatio="none"
+       xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAC+lBMVEUAAAAAAP8AAIAAAFUAAGYA&#10;AEYAAE4AAEoAAEYAAEYAAEQAAEgAAEYAAEUAAEQAAEUAAEUAAEUAAEUAAEUAAEQAAEQAAEMAAEQA&#10;AEQAAEQAAEMAAEQAAEMAAEQAAEMAAEQAAEQAAEQAAEQAAEMAAEIAAEQAAEMAAEQAAEP///////7+&#10;/v/+/v79/f79/f38/P38/Pz7+/z7+/v6+vv6+vr5+fr5+fn4+Pj29vf19fb09PXy8vPw8PLw8PHu&#10;7u/s7O7q6uzn6Orm5ujk5Obj4+bj4+Xi4uTg4OPe3uHc3N/b297a2t3Z2dzY2NvX19rW1trW1tjV&#10;1dnU1NfT09fT09bS0tbPz9POztLLy8/Jyc3GxsvGxsrFxcrFxcnDw8jBwcbAwMW/v8S8vMK6usC5&#10;ub+3t722try1tbuysrmvsLaurrWrq7KoqLCnp66kpKyioquhoamenqednaabm6SZmaKYmKKXl6GW&#10;lqCUlJ6Tk52RkZyOjpiLi5aKipWHh5OEhJGEhJCCgo6AgI2AgIx9fYp7e4l5eYZ2doRxcYBubn5q&#10;a3tpaXlnZ3llZXdiYnRhYXNeXnFdXXFcXHBaWm5YWG1VVWtTU2lQUGdNTWVKSmNJSWJGRmFGRmBE&#10;RWBERF9CQ15AQV0+Pls7PFk3N1c0NVYxMVQwMFMuLlItLVIrLFEqKk8oKE4mJk4mJk0kJEwjI0si&#10;IkshIUshIUogIEwgIEofH0wfH0oeHkoeHkkdHUkcHEkaGkkaGkgZGUgYGEcXF0gWFkcVFUcTE0cT&#10;E0YSEkYREUYREUUQEEYQEEUPD0UODkUNDUULC0UKCkQJCUQICEYHCEQGBkQFBUQEBEMDA0MCA0UC&#10;AkMBAUMAAEMAAEIAAEEAAD8AAD4AAD0AADoAADkAADgAADYAADUAADMAADIAADEAADAAAC4AACwA&#10;ACsAACoAACgAACYAACQAACIAACEAAB8AABsAABoAABgAABQAABMAABEAAA4AAAwAAAgAAAYAAAQA&#10;AAEAAACcat6QAAAAKXRSTlMAAQIDBQsNGCEoKS43P0BOVV1nb3GPlJmdqLK5usrU5en09fr7/Pz9&#10;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&#10;"
+       id="image1" />
+  </g>
+</svg>
diff --git a/music_assistant/providers/lrclib/manifest.json b/music_assistant/providers/lrclib/manifest.json
new file mode 100644 (file)
index 0000000..7eb8be3
--- /dev/null
@@ -0,0 +1,13 @@
+{
+  "type": "metadata",
+  "domain": "lrclib",
+  "name": "LRCLIB",
+  "description": "LRCLIB is a completely free service for finding and contributing synchronized lyrics, with an easy-to-use and machine-friendly API.",
+  "codeowners": ["@music-assistant"],
+  "requirements": [],
+  "documentation": "",
+  "multi_instance": false,
+  "builtin": true,
+  "allow_disable": true,
+  "icon": "mdi-folder-information"
+}