* 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>
193 lines
7.4 KiB
Rust
193 lines
7.4 KiB
Rust
// Code generated by ndpgen. DO NOT EDIT.
|
|
//
|
|
// This file contains export wrappers for the Scrobbler 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 }
|
|
/// ScrobblerError represents an error type for scrobbling operations.
|
|
pub type ScrobblerError = &'static str;
|
|
/// ScrobblerErrorNotAuthorized indicates the user is not authorized.
|
|
pub const SCROBBLER_ERROR_NOT_AUTHORIZED: ScrobblerError = "scrobbler(not_authorized)";
|
|
/// ScrobblerErrorRetryLater indicates the operation should be retried later.
|
|
pub const SCROBBLER_ERROR_RETRY_LATER: ScrobblerError = "scrobbler(retry_later)";
|
|
/// ScrobblerErrorUnrecoverable indicates an unrecoverable error.
|
|
pub const SCROBBLER_ERROR_UNRECOVERABLE: ScrobblerError = "scrobbler(unrecoverable)";
|
|
/// 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,
|
|
}
|
|
/// IsAuthorizedRequest is the request for authorization check.
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct IsAuthorizedRequest {
|
|
/// Username is the username of the user.
|
|
#[serde(default)]
|
|
pub username: String,
|
|
}
|
|
/// NowPlayingRequest is the request for now playing notification.
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct NowPlayingRequest {
|
|
/// Username is the username of the user.
|
|
#[serde(default)]
|
|
pub username: String,
|
|
/// Track is the track currently playing.
|
|
#[serde(default)]
|
|
pub track: TrackInfo,
|
|
/// Position is the current playback position in seconds.
|
|
#[serde(default)]
|
|
pub position: i32,
|
|
}
|
|
/// ScrobbleRequest is the request for submitting a scrobble.
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ScrobbleRequest {
|
|
/// Username is the username of the user.
|
|
#[serde(default)]
|
|
pub username: String,
|
|
/// Track is the track that was played.
|
|
#[serde(default)]
|
|
pub track: TrackInfo,
|
|
/// Timestamp is the Unix timestamp when the track started playing.
|
|
#[serde(default)]
|
|
pub timestamp: i64,
|
|
}
|
|
/// 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() }
|
|
}
|
|
}
|
|
|
|
/// Scrobbler requires all methods to be implemented.
|
|
/// Scrobbler provides scrobbling functionality to external services.
|
|
/// This capability allows plugins to submit listening history to services like Last.fm,
|
|
/// ListenBrainz, or custom scrobbling backends.
|
|
///
|
|
/// All methods are required - plugins implementing this capability must provide
|
|
/// all three functions: IsAuthorized, NowPlaying, and Scrobble.
|
|
pub trait Scrobbler {
|
|
/// IsAuthorized - IsAuthorized checks if a user is authorized to scrobble to this service.
|
|
fn is_authorized(&self, req: IsAuthorizedRequest) -> Result<bool, Error>;
|
|
/// NowPlaying - NowPlaying sends a now playing notification to the scrobbling service.
|
|
fn now_playing(&self, req: NowPlayingRequest) -> Result<(), Error>;
|
|
/// Scrobble - Scrobble submits a completed scrobble to the scrobbling service.
|
|
fn scrobble(&self, req: ScrobbleRequest) -> Result<(), Error>;
|
|
}
|
|
|
|
/// Register all exports for the Scrobbler capability.
|
|
/// This macro generates the WASM export functions for all trait methods.
|
|
#[macro_export]
|
|
macro_rules! register_scrobbler {
|
|
($plugin_type:ty) => {
|
|
#[extism_pdk::plugin_fn]
|
|
pub fn nd_scrobbler_is_authorized(
|
|
req: extism_pdk::Json<$crate::scrobbler::IsAuthorizedRequest>
|
|
) -> extism_pdk::FnResult<extism_pdk::Json<bool>> {
|
|
let plugin = <$plugin_type>::default();
|
|
let result = $crate::scrobbler::Scrobbler::is_authorized(&plugin, req.into_inner())?;
|
|
Ok(extism_pdk::Json(result))
|
|
}
|
|
#[extism_pdk::plugin_fn]
|
|
pub fn nd_scrobbler_now_playing(
|
|
req: extism_pdk::Json<$crate::scrobbler::NowPlayingRequest>
|
|
) -> extism_pdk::FnResult<()> {
|
|
let plugin = <$plugin_type>::default();
|
|
$crate::scrobbler::Scrobbler::now_playing(&plugin, req.into_inner())?;
|
|
Ok(())
|
|
}
|
|
#[extism_pdk::plugin_fn]
|
|
pub fn nd_scrobbler_scrobble(
|
|
req: extism_pdk::Json<$crate::scrobbler::ScrobbleRequest>
|
|
) -> extism_pdk::FnResult<()> {
|
|
let plugin = <$plugin_type>::default();
|
|
$crate::scrobbler::Scrobbler::scrobble(&plugin, req.into_inner())?;
|
|
Ok(())
|
|
}
|
|
};
|
|
}
|