feat(plugins): add lyrics provider plugin capability (#5126)

* feat(plugins): add lyrics provider plugin capability

Refactor the lyrics system from a static function to an interface-based
service that supports WASM plugin providers. Plugins listed in the
LyricsPriority config (alongside "embedded" and file extensions) are
now resolved through the plugin system.

Includes capability definition, Go/Rust PDK, adapter, Wire integration,
and tests for plugin fallback behavior.

* test(plugins): add lyrics capability integration test with test plugin

* fix(plugins): default lyrics language to 'xxx' when plugin omits it

Per the OpenSubsonic spec, the server must return 'und' or 'xxx' when
the lyrics language is unknown. The lyrics plugin adapter was passing
an empty string through when a plugin didn't provide a language value.
This defaults the language to 'xxx', consistent with all other callers
of model.ToLyrics() in the codebase.

* refactor(plugins): rename lyrics import to improve clarity

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(lyrics): update TrackInfo description for clarity

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(lyrics): enhance lyrics plugin handling and case sensitivity

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(plugins): update payload type to string with byte format for task data

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2026-03-03 15:48:39 -05:00
committed by GitHub
parent eeb1bd5f41
commit f03ca44a8e
33 changed files with 930 additions and 28 deletions
@@ -6,6 +6,7 @@
//! for implementing Navidrome plugin capabilities in Rust.
pub mod lifecycle;
pub mod lyrics;
pub mod metadata;
pub mod scheduler;
pub mod scrobbler;
@@ -0,0 +1,148 @@
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains export wrappers for the Lyrics capability.
// It is intended for use in Navidrome plugins built with extism-pdk.
use serde::{Deserialize, Serialize};
// Helper functions for skip_serializing_if with numeric types
#[allow(dead_code)]
fn is_zero_i32(value: &i32) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_u32(value: &u32) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_i64(value: &i64) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_u64(value: &u64) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_f32(value: &f32) -> bool { *value == 0.0 }
#[allow(dead_code)]
fn is_zero_f64(value: &f64) -> bool { *value == 0.0 }
/// ArtistRef is a reference to an artist with name and optional MBID.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtistRef {
/// ID is the internal Navidrome artist ID (if known).
#[serde(default, skip_serializing_if = "String::is_empty")]
pub id: String,
/// Name is the artist name.
#[serde(default)]
pub name: String,
/// MBID is the MusicBrainz ID for the artist.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub mbid: String,
}
/// GetLyricsRequest contains the track information for lyrics lookup.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetLyricsRequest {
#[serde(default)]
pub track: TrackInfo,
}
/// GetLyricsResponse contains the lyrics returned by the plugin.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetLyricsResponse {
#[serde(default)]
pub lyrics: Vec<LyricsText>,
}
/// LyricsText represents a single set of lyrics in raw text format.
/// Text can be plain text or LRC format — Navidrome will parse it.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LyricsText {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub lang: String,
#[serde(default)]
pub text: String,
}
/// TrackInfo contains track metadata.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrackInfo {
/// ID is the internal Navidrome track ID.
#[serde(default)]
pub id: String,
/// Title is the track title.
#[serde(default)]
pub title: String,
/// Album is the album name.
#[serde(default)]
pub album: String,
/// Artist is the formatted artist name for display (e.g., "Artist1 • Artist2").
#[serde(default)]
pub artist: String,
/// AlbumArtist is the formatted album artist name for display.
#[serde(default)]
pub album_artist: String,
/// Artists is the list of track artists.
#[serde(default)]
pub artists: Vec<ArtistRef>,
/// AlbumArtists is the list of album artists.
#[serde(default)]
pub album_artists: Vec<ArtistRef>,
/// Duration is the track duration in seconds.
#[serde(default)]
pub duration: f32,
/// TrackNumber is the track number on the album.
#[serde(default)]
pub track_number: i32,
/// DiscNumber is the disc number.
#[serde(default)]
pub disc_number: i32,
/// MBZRecordingID is the MusicBrainz recording ID.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub mbz_recording_id: String,
/// MBZAlbumID is the MusicBrainz album/release ID.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub mbz_album_id: String,
/// MBZReleaseGroupID is the MusicBrainz release group ID.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub mbz_release_group_id: String,
/// MBZReleaseTrackID is the MusicBrainz release track ID.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub mbz_release_track_id: String,
}
/// Error represents an error from a capability method.
#[derive(Debug)]
pub struct Error {
pub message: String,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for Error {}
impl Error {
pub fn new(message: impl Into<String>) -> Self {
Self { message: message.into() }
}
}
/// Lyrics requires all methods to be implemented.
/// Lyrics provides lyrics for a given track from external sources.
pub trait Lyrics {
/// GetLyrics
fn get_lyrics(&self, req: GetLyricsRequest) -> Result<GetLyricsResponse, Error>;
}
/// Register all exports for the Lyrics capability.
/// This macro generates the WASM export functions for all trait methods.
#[macro_export]
macro_rules! register_lyrics {
($plugin_type:ty) => {
#[extism_pdk::plugin_fn]
pub fn nd_lyrics_get_lyrics(
req: extism_pdk::Json<$crate::lyrics::GetLyricsRequest>
) -> extism_pdk::FnResult<extism_pdk::Json<$crate::lyrics::GetLyricsResponse>> {
let plugin = <$plugin_type>::default();
let result = $crate::lyrics::Lyrics::get_lyrics(&plugin, req.into_inner())?;
Ok(extism_pdk::Json(result))
}
};
}
@@ -76,7 +76,7 @@ pub struct ScrobbleRequest {
#[serde(default)]
pub timestamp: i64,
}
/// TrackInfo contains track metadata for scrobbling.
/// TrackInfo contains track metadata.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrackInfo {