mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
refactor(plugins): update scrobbler interface to return errors directly instead of response structs
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
306b2fc525
commit
9ac0764cc2
@ -15,11 +15,11 @@ type Scrobbler interface {
|
||||
|
||||
// NowPlaying sends a now playing notification to the scrobbling service.
|
||||
//nd:export name=nd_scrobbler_now_playing
|
||||
NowPlaying(NowPlayingRequest) (*ScrobblerResponse, error)
|
||||
NowPlaying(NowPlayingRequest) error
|
||||
|
||||
// Scrobble submits a completed scrobble to the scrobbling service.
|
||||
//nd:export name=nd_scrobbler_scrobble
|
||||
Scrobble(ScrobbleRequest) (*ScrobblerResponse, error)
|
||||
Scrobble(ScrobbleRequest) error
|
||||
}
|
||||
|
||||
// IsAuthorizedRequest is the request for authorization check.
|
||||
@ -92,24 +92,17 @@ type ScrobbleRequest struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// ScrobblerErrorType indicates how Navidrome should handle scrobbler errors.
|
||||
type ScrobblerErrorType string
|
||||
// ScrobblerError represents an error type for scrobbling operations.
|
||||
type ScrobblerError string
|
||||
|
||||
const (
|
||||
// ScrobblerErrorNone indicates no error occurred.
|
||||
ScrobblerErrorNone ScrobblerErrorType = "none"
|
||||
// ScrobblerErrorNotAuthorized indicates the user is not authorized.
|
||||
ScrobblerErrorNotAuthorized ScrobblerErrorType = "not_authorized"
|
||||
ScrobblerErrorNotAuthorized ScrobblerError = "scrobbler(not_authorized)"
|
||||
// ScrobblerErrorRetryLater indicates the operation should be retried later.
|
||||
ScrobblerErrorRetryLater ScrobblerErrorType = "retry_later"
|
||||
ScrobblerErrorRetryLater ScrobblerError = "scrobbler(retry_later)"
|
||||
// ScrobblerErrorUnrecoverable indicates an unrecoverable error.
|
||||
ScrobblerErrorUnrecoverable ScrobblerErrorType = "unrecoverable"
|
||||
ScrobblerErrorUnrecoverable ScrobblerError = "scrobbler(unrecoverable)"
|
||||
)
|
||||
|
||||
// ScrobblerResponse is the response for scrobbler operations.
|
||||
type ScrobblerResponse struct {
|
||||
// Error is the error message if the operation failed.
|
||||
Error string `json:"error,omitempty"`
|
||||
// ErrorType indicates how Navidrome should handle the error.
|
||||
ErrorType ScrobblerErrorType `json:"errorType,omitempty"`
|
||||
}
|
||||
// Error implements the error interface for ScrobblerError.
|
||||
func (e ScrobblerError) Error() string { return string(e) }
|
||||
|
||||
@ -13,17 +13,11 @@ exports:
|
||||
input:
|
||||
$ref: '#/components/schemas/NowPlayingRequest'
|
||||
contentType: application/json
|
||||
output:
|
||||
$ref: '#/components/schemas/ScrobblerResponse'
|
||||
contentType: application/json
|
||||
nd_scrobbler_scrobble:
|
||||
description: Scrobble submits a completed scrobble to the scrobbling service.
|
||||
input:
|
||||
$ref: '#/components/schemas/ScrobbleRequest'
|
||||
contentType: application/json
|
||||
output:
|
||||
$ref: '#/components/schemas/ScrobblerResponse'
|
||||
contentType: application/json
|
||||
components:
|
||||
schemas:
|
||||
IsAuthorizedRequest:
|
||||
@ -92,16 +86,6 @@ components:
|
||||
- username
|
||||
- track
|
||||
- timestamp
|
||||
ScrobblerResponse:
|
||||
description: ScrobblerResponse is the response for scrobbler operations.
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error is the error message if the operation failed.
|
||||
errorType:
|
||||
$ref: '#/components/schemas/ScrobblerErrorType'
|
||||
description: ErrorType indicates how Navidrome should handle the error.
|
||||
TrackInfo:
|
||||
description: TrackInfo contains track metadata for scrobbling.
|
||||
type: object
|
||||
@ -160,11 +144,10 @@ components:
|
||||
- duration
|
||||
- trackNumber
|
||||
- discNumber
|
||||
ScrobblerErrorType:
|
||||
description: ScrobblerErrorType indicates how Navidrome should handle scrobbler errors.
|
||||
ScrobblerError:
|
||||
description: ScrobblerError represents an error type for scrobbling operations.
|
||||
type: string
|
||||
enum:
|
||||
- none
|
||||
- not_authorized
|
||||
- retry_later
|
||||
- unrecoverable
|
||||
- scrobbler(not_authorized)
|
||||
- scrobbler(retry_later)
|
||||
- scrobbler(unrecoverable)
|
||||
|
||||
@ -194,6 +194,16 @@ func parseCapabilityFile(fset *token.FileSet, path string) ([]Capability, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Also attach type aliases prefixed with interface name (e.g., ScrobblerError for Scrobbler interface)
|
||||
// This supports error types that are not directly referenced in method signatures
|
||||
interfaceName := typeSpec.Name.Name
|
||||
for typeName, a := range aliasMap {
|
||||
if strings.HasPrefix(typeName, interfaceName) && !referencedTypes[typeName] {
|
||||
capability.TypeAliases = append(capability.TypeAliases, a)
|
||||
referencedTypes[typeName] = true // Mark as referenced for const lookup
|
||||
}
|
||||
}
|
||||
|
||||
// Attach const groups that match referenced type aliases
|
||||
for _, group := range allConstGroups {
|
||||
if group.Type == "" {
|
||||
|
||||
@ -40,6 +40,21 @@ const (
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate Error() methods for string type aliases with const values (implements error interface) */ -}}
|
||||
{{- $consts := .Capability.Consts}}
|
||||
{{- range .Capability.TypeAliases}}
|
||||
{{- if eq .Type "string"}}
|
||||
{{- $typeName := .Name}}
|
||||
{{- range $consts}}
|
||||
{{- if eq .Type $typeName}}
|
||||
|
||||
// Error implements the error interface for {{$typeName}}.
|
||||
func (e {{$typeName}}) Error() string { return string(e) }
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate struct definitions */ -}}
|
||||
{{- range .Capability.Structs}}
|
||||
|
||||
|
||||
@ -37,6 +37,21 @@ const (
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate Error() methods for string type aliases with const values (implements error interface) */ -}}
|
||||
{{- $consts := .Capability.Consts}}
|
||||
{{- range .Capability.TypeAliases}}
|
||||
{{- if eq .Type "string"}}
|
||||
{{- $typeName := .Name}}
|
||||
{{- range $consts}}
|
||||
{{- if eq .Type $typeName}}
|
||||
|
||||
// Error implements the error interface for {{$typeName}}.
|
||||
func (e {{$typeName}}) Error() string { return string(e) }
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate struct definitions */ -}}
|
||||
{{- range .Capability.Structs}}
|
||||
|
||||
|
||||
@ -129,15 +129,6 @@ struct ScrobbleInput {
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ScrobblerOutput {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error_type: Option<String>,
|
||||
}
|
||||
|
||||
const ERROR_TYPE_NOT_AUTHORIZED: &str = "not_authorized";
|
||||
const ERROR_TYPE_RETRY_LATER: &str = "retry_later";
|
||||
|
||||
@ -216,9 +207,7 @@ pub fn nd_scrobbler_is_authorized(Json(input): Json<AuthInput>) -> FnResult<Json
|
||||
|
||||
/// Sends a now playing notification to Discord.
|
||||
#[plugin_fn]
|
||||
pub fn nd_scrobbler_now_playing(
|
||||
Json(input): Json<NowPlayingInput>,
|
||||
) -> FnResult<Json<Option<ScrobblerOutput>>> {
|
||||
pub fn nd_scrobbler_now_playing(Json(input): Json<NowPlayingInput>) -> FnResult<()> {
|
||||
info!(
|
||||
"Setting presence for user {}, track: {}",
|
||||
input.username, input.track.title
|
||||
@ -228,11 +217,10 @@ pub fn nd_scrobbler_now_playing(
|
||||
let (client_id, users) = match get_config() {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
let err_msg = format!("failed to get config: {:?}", e);
|
||||
return Ok(Json(Some(ScrobblerOutput {
|
||||
error: Some(err_msg),
|
||||
error_type: Some(ERROR_TYPE_RETRY_LATER.to_string()),
|
||||
})));
|
||||
return Err(WithReturnCode::new(
|
||||
Error::msg(format!("{}: failed to get config: {:?}", ERROR_TYPE_RETRY_LATER, e)),
|
||||
-1,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@ -240,21 +228,25 @@ pub fn nd_scrobbler_now_playing(
|
||||
let user_token = match users.get(&input.username) {
|
||||
Some(token) => token.clone(),
|
||||
None => {
|
||||
let err_msg = format!("user '{}' not authorized", input.username);
|
||||
return Ok(Json(Some(ScrobblerOutput {
|
||||
error: Some(err_msg),
|
||||
error_type: Some(ERROR_TYPE_NOT_AUTHORIZED.to_string()),
|
||||
})));
|
||||
return Err(WithReturnCode::new(
|
||||
Error::msg(format!(
|
||||
"{}: user '{}' not authorized",
|
||||
ERROR_TYPE_NOT_AUTHORIZED, input.username
|
||||
)),
|
||||
-1,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to Discord
|
||||
if let Err(e) = rpc::connect(&input.username, &user_token) {
|
||||
let err_msg = format!("failed to connect to Discord: {:?}", e);
|
||||
return Ok(Json(Some(ScrobblerOutput {
|
||||
error: Some(err_msg),
|
||||
error_type: Some(ERROR_TYPE_RETRY_LATER.to_string()),
|
||||
})));
|
||||
return Err(WithReturnCode::new(
|
||||
Error::msg(format!(
|
||||
"{}: failed to connect to Discord: {:?}",
|
||||
ERROR_TYPE_RETRY_LATER, e
|
||||
)),
|
||||
-1,
|
||||
));
|
||||
}
|
||||
|
||||
// Cancel any existing completion schedule
|
||||
@ -289,11 +281,13 @@ pub fn nd_scrobbler_now_playing(
|
||||
},
|
||||
},
|
||||
) {
|
||||
let err_msg = format!("failed to send activity: {:?}", e);
|
||||
return Ok(Json(Some(ScrobblerOutput {
|
||||
error: Some(err_msg),
|
||||
error_type: Some(ERROR_TYPE_RETRY_LATER.to_string()),
|
||||
})));
|
||||
return Err(WithReturnCode::new(
|
||||
Error::msg(format!(
|
||||
"{}: failed to send activity: {:?}",
|
||||
ERROR_TYPE_RETRY_LATER, e
|
||||
)),
|
||||
-1,
|
||||
));
|
||||
}
|
||||
|
||||
// Schedule a timer to clear the activity after the track completes
|
||||
@ -306,15 +300,14 @@ pub fn nd_scrobbler_now_playing(
|
||||
warn!("Failed to schedule completion timer: {:?}", e);
|
||||
}
|
||||
|
||||
// Success - return None to indicate no error
|
||||
Ok(Json(None))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles scrobble requests (no-op for Discord Rich Presence).
|
||||
#[plugin_fn]
|
||||
pub fn nd_scrobbler_scrobble(_input: Json<ScrobbleInput>) -> FnResult<Json<Option<ScrobblerOutput>>> {
|
||||
pub fn nd_scrobbler_scrobble(_input: Json<ScrobbleInput>) -> FnResult<()> {
|
||||
// Discord Rich Presence doesn't need scrobble events - success
|
||||
Ok(Json(None))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@ -105,30 +105,24 @@ func (p *discordPlugin) IsAuthorized(input scrobbler.IsAuthorizedRequest) (*scro
|
||||
}
|
||||
|
||||
// NowPlaying sends a now playing notification to Discord.
|
||||
func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) (*scrobbler.ScrobblerResponse, error) {
|
||||
func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
|
||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Setting presence for user %s, track: %s", input.Username, input.Track.Title))
|
||||
|
||||
// Load configuration
|
||||
clientID, users, err := getConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config: %w", err)
|
||||
return fmt.Errorf("%w: failed to get config: %v", scrobbler.ScrobblerErrorRetryLater, err)
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
userToken, authorized := users[input.Username]
|
||||
if !authorized {
|
||||
return &scrobbler.ScrobblerResponse{
|
||||
Error: fmt.Sprintf("user '%s' not authorized", input.Username),
|
||||
ErrorType: scrobbler.ScrobblerErrorNotAuthorized,
|
||||
}, nil
|
||||
return fmt.Errorf("%w: user '%s' not authorized", scrobbler.ScrobblerErrorNotAuthorized, input.Username)
|
||||
}
|
||||
|
||||
// Connect to Discord
|
||||
if err := connect(input.Username, userToken); err != nil {
|
||||
return &scrobbler.ScrobblerResponse{
|
||||
Error: fmt.Sprintf("failed to connect to Discord: %v", err),
|
||||
ErrorType: scrobbler.ScrobblerErrorRetryLater,
|
||||
}, nil
|
||||
return fmt.Errorf("%w: failed to connect to Discord: %v", scrobbler.ScrobblerErrorRetryLater, err)
|
||||
}
|
||||
|
||||
// Cancel any existing completion schedule
|
||||
@ -155,10 +149,7 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) (*scrobble
|
||||
LargeText: input.Track.Album,
|
||||
},
|
||||
}); err != nil {
|
||||
return &scrobbler.ScrobblerResponse{
|
||||
Error: fmt.Sprintf("failed to send activity: %v", err),
|
||||
ErrorType: scrobbler.ScrobblerErrorRetryLater,
|
||||
}, nil
|
||||
return fmt.Errorf("%w: failed to send activity: %v", scrobbler.ScrobblerErrorRetryLater, err)
|
||||
}
|
||||
|
||||
// Schedule a timer to clear the activity after the track completes
|
||||
@ -168,13 +159,13 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) (*scrobble
|
||||
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to schedule completion timer: %v", err))
|
||||
}
|
||||
|
||||
return &scrobbler.ScrobblerResponse{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scrobble handles scrobble requests (no-op for Discord).
|
||||
func (p *discordPlugin) Scrobble(_ scrobbler.ScrobbleRequest) (*scrobbler.ScrobblerResponse, error) {
|
||||
func (p *discordPlugin) Scrobble(_ scrobbler.ScrobbleRequest) error {
|
||||
// Discord Rich Presence doesn't need scrobble events
|
||||
return &scrobbler.ScrobblerResponse{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@ -13,71 +13,16 @@ import (
|
||||
|
||||
var errFunctionNotFound = errors.New("function not found")
|
||||
|
||||
// callPluginFunctionNoInput calls a plugin function that takes no input.
|
||||
// It only checks for errors, with no response expected.
|
||||
// callPluginFunctionNoInput is a helper to call a plugin function with no input and output.
|
||||
func callPluginFunctionNoInput(ctx context.Context, plugin *plugin, funcName string) error {
|
||||
start := time.Now()
|
||||
|
||||
// Create plugin instance
|
||||
p, err := plugin.instance()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create plugin: %w", err)
|
||||
}
|
||||
defer p.Close(ctx)
|
||||
|
||||
if !p.FunctionExists(funcName) {
|
||||
log.Trace(ctx, "Plugin function not found", "plugin", plugin.name, "function", funcName)
|
||||
return fmt.Errorf("%w: %s", errFunctionNotFound, funcName)
|
||||
}
|
||||
|
||||
startCall := time.Now()
|
||||
exit, _, err := p.Call(funcName, nil)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Plugin call failed", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall), "navidromeDuration", startCall.Sub(start), err)
|
||||
return fmt.Errorf("plugin call failed: %w", err)
|
||||
}
|
||||
if exit != 0 {
|
||||
return fmt.Errorf("plugin call exited with code %d", exit)
|
||||
}
|
||||
|
||||
log.Trace(ctx, "Plugin call succeeded", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall), "navidromeDuration", startCall.Sub(start))
|
||||
return nil
|
||||
_, err := callPluginFunction[struct{}, struct{}](ctx, plugin, funcName, struct{}{})
|
||||
return err
|
||||
}
|
||||
|
||||
// callPluginFunctionNoOutput calls a plugin function with input but no response expected.
|
||||
// It handles JSON marshalling for input and only checks for errors.
|
||||
// callPluginFunctionNoOutput is a helper to call a plugin function with input and no output.
|
||||
func callPluginFunctionNoOutput[I any](ctx context.Context, plugin *plugin, funcName string, input I) error {
|
||||
start := time.Now()
|
||||
|
||||
// Create plugin instance
|
||||
p, err := plugin.instance()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create plugin: %w", err)
|
||||
}
|
||||
defer p.Close(ctx)
|
||||
|
||||
if !p.FunctionExists(funcName) {
|
||||
log.Trace(ctx, "Plugin function not found", "plugin", plugin.name, "function", funcName)
|
||||
return fmt.Errorf("%w: %s", errFunctionNotFound, funcName)
|
||||
}
|
||||
|
||||
inputBytes, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal input: %w", err)
|
||||
}
|
||||
|
||||
startCall := time.Now()
|
||||
exit, _, err := p.Call(funcName, inputBytes)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Plugin call failed", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall), "navidromeDuration", startCall.Sub(start), err)
|
||||
return fmt.Errorf("plugin call failed: %w", err)
|
||||
}
|
||||
if exit != 0 {
|
||||
return fmt.Errorf("plugin call exited with code %d", exit)
|
||||
}
|
||||
|
||||
log.Trace(ctx, "Plugin call succeeded", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall), "navidromeDuration", startCall.Sub(start))
|
||||
return nil
|
||||
_, err := callPluginFunction[I, struct{}](ctx, plugin, funcName, input)
|
||||
return err
|
||||
}
|
||||
|
||||
// callPluginFunction is a helper to call a plugin function with input and output types.
|
||||
|
||||
@ -11,6 +11,108 @@ import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// ArtistBiographyResponse is the response for GetArtistBiography.
|
||||
type ArtistBiographyResponse struct {
|
||||
// Biography is the artist biography text.
|
||||
Biography string `json:"biography"`
|
||||
}
|
||||
|
||||
// SimilarArtistsResponse is the response for GetSimilarArtists.
|
||||
type SimilarArtistsResponse struct {
|
||||
// Artists is the list of similar artists.
|
||||
Artists []ArtistRef `json:"artists"`
|
||||
}
|
||||
|
||||
// TopSongsResponse is the response for GetArtistTopSongs.
|
||||
type TopSongsResponse struct {
|
||||
// Songs is the list of top songs.
|
||||
Songs []SongRef `json:"songs"`
|
||||
}
|
||||
|
||||
// ArtistRef is a reference to an artist with name and optional MBID.
|
||||
type ArtistRef struct {
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistImagesResponse is the response for GetArtistImages.
|
||||
type ArtistImagesResponse struct {
|
||||
// Images is the list of artist images.
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// AlbumInfoResponse is the response for GetAlbumInfo.
|
||||
type AlbumInfoResponse struct {
|
||||
// Name is the album name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the album.
|
||||
MBID string `json:"mbid"`
|
||||
// Description is the album description/notes.
|
||||
Description string `json:"description"`
|
||||
// URL is the external URL for the album.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// ImageInfo represents an image with URL and size.
|
||||
type ImageInfo struct {
|
||||
// URL is the URL of the image.
|
||||
URL string `json:"url"`
|
||||
// Size is the size of the image in pixels (width or height).
|
||||
Size int32 `json:"size"`
|
||||
}
|
||||
|
||||
// ArtistMBIDResponse is the response for GetArtistMBID.
|
||||
type ArtistMBIDResponse struct {
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid"`
|
||||
}
|
||||
|
||||
// ArtistURLResponse is the response for GetArtistURL.
|
||||
type ArtistURLResponse struct {
|
||||
// URL is the external URL for the artist.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// SongRef is a reference to a song with name and optional MBID.
|
||||
type SongRef struct {
|
||||
// Name is the song name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the song.
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistMBIDRequest is the request for GetArtistMBID.
|
||||
type ArtistMBIDRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ArtistRequest is the common request for artist-related functions.
|
||||
type ArtistRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// SimilarArtistsRequest is the request for GetSimilarArtists.
|
||||
type SimilarArtistsRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
// Limit is the maximum number of similar artists to return.
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
// TopSongsRequest is the request for GetArtistTopSongs.
|
||||
type TopSongsRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
@ -39,108 +141,6 @@ type AlbumImagesResponse struct {
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// ImageInfo represents an image with URL and size.
|
||||
type ImageInfo struct {
|
||||
// URL is the URL of the image.
|
||||
URL string `json:"url"`
|
||||
// Size is the size of the image in pixels (width or height).
|
||||
Size int32 `json:"size"`
|
||||
}
|
||||
|
||||
// ArtistRequest is the common request for artist-related functions.
|
||||
type ArtistRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistBiographyResponse is the response for GetArtistBiography.
|
||||
type ArtistBiographyResponse struct {
|
||||
// Biography is the artist biography text.
|
||||
Biography string `json:"biography"`
|
||||
}
|
||||
|
||||
// SimilarArtistsRequest is the request for GetSimilarArtists.
|
||||
type SimilarArtistsRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
// Limit is the maximum number of similar artists to return.
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
// TopSongsResponse is the response for GetArtistTopSongs.
|
||||
type TopSongsResponse struct {
|
||||
// Songs is the list of top songs.
|
||||
Songs []SongRef `json:"songs"`
|
||||
}
|
||||
|
||||
// AlbumInfoResponse is the response for GetAlbumInfo.
|
||||
type AlbumInfoResponse struct {
|
||||
// Name is the album name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the album.
|
||||
MBID string `json:"mbid"`
|
||||
// Description is the album description/notes.
|
||||
Description string `json:"description"`
|
||||
// URL is the external URL for the album.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// ArtistRef is a reference to an artist with name and optional MBID.
|
||||
type ArtistRef struct {
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistURLResponse is the response for GetArtistURL.
|
||||
type ArtistURLResponse struct {
|
||||
// URL is the external URL for the artist.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// SimilarArtistsResponse is the response for GetSimilarArtists.
|
||||
type SimilarArtistsResponse struct {
|
||||
// Artists is the list of similar artists.
|
||||
Artists []ArtistRef `json:"artists"`
|
||||
}
|
||||
|
||||
// ArtistImagesResponse is the response for GetArtistImages.
|
||||
type ArtistImagesResponse struct {
|
||||
// Images is the list of artist images.
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// SongRef is a reference to a song with name and optional MBID.
|
||||
type SongRef struct {
|
||||
// Name is the song name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the song.
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistMBIDRequest is the request for GetArtistMBID.
|
||||
type ArtistMBIDRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ArtistMBIDResponse is the response for GetArtistMBID.
|
||||
type ArtistMBIDResponse struct {
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid"`
|
||||
}
|
||||
|
||||
// Metadata is the marker interface for metadata plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// MetadataAgent provides artist and album metadata retrieval.
|
||||
|
||||
@ -8,6 +8,108 @@
|
||||
|
||||
package metadata
|
||||
|
||||
// ArtistBiographyResponse is the response for GetArtistBiography.
|
||||
type ArtistBiographyResponse struct {
|
||||
// Biography is the artist biography text.
|
||||
Biography string `json:"biography"`
|
||||
}
|
||||
|
||||
// SimilarArtistsResponse is the response for GetSimilarArtists.
|
||||
type SimilarArtistsResponse struct {
|
||||
// Artists is the list of similar artists.
|
||||
Artists []ArtistRef `json:"artists"`
|
||||
}
|
||||
|
||||
// TopSongsResponse is the response for GetArtistTopSongs.
|
||||
type TopSongsResponse struct {
|
||||
// Songs is the list of top songs.
|
||||
Songs []SongRef `json:"songs"`
|
||||
}
|
||||
|
||||
// ArtistRef is a reference to an artist with name and optional MBID.
|
||||
type ArtistRef struct {
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistImagesResponse is the response for GetArtistImages.
|
||||
type ArtistImagesResponse struct {
|
||||
// Images is the list of artist images.
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// AlbumInfoResponse is the response for GetAlbumInfo.
|
||||
type AlbumInfoResponse struct {
|
||||
// Name is the album name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the album.
|
||||
MBID string `json:"mbid"`
|
||||
// Description is the album description/notes.
|
||||
Description string `json:"description"`
|
||||
// URL is the external URL for the album.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// ImageInfo represents an image with URL and size.
|
||||
type ImageInfo struct {
|
||||
// URL is the URL of the image.
|
||||
URL string `json:"url"`
|
||||
// Size is the size of the image in pixels (width or height).
|
||||
Size int32 `json:"size"`
|
||||
}
|
||||
|
||||
// ArtistMBIDResponse is the response for GetArtistMBID.
|
||||
type ArtistMBIDResponse struct {
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid"`
|
||||
}
|
||||
|
||||
// ArtistURLResponse is the response for GetArtistURL.
|
||||
type ArtistURLResponse struct {
|
||||
// URL is the external URL for the artist.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// SongRef is a reference to a song with name and optional MBID.
|
||||
type SongRef struct {
|
||||
// Name is the song name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the song.
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistMBIDRequest is the request for GetArtistMBID.
|
||||
type ArtistMBIDRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ArtistRequest is the common request for artist-related functions.
|
||||
type ArtistRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// SimilarArtistsRequest is the request for GetSimilarArtists.
|
||||
type SimilarArtistsRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
// Limit is the maximum number of similar artists to return.
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
// TopSongsRequest is the request for GetArtistTopSongs.
|
||||
type TopSongsRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
@ -36,108 +138,6 @@ type AlbumImagesResponse struct {
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// ImageInfo represents an image with URL and size.
|
||||
type ImageInfo struct {
|
||||
// URL is the URL of the image.
|
||||
URL string `json:"url"`
|
||||
// Size is the size of the image in pixels (width or height).
|
||||
Size int32 `json:"size"`
|
||||
}
|
||||
|
||||
// ArtistRequest is the common request for artist-related functions.
|
||||
type ArtistRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistBiographyResponse is the response for GetArtistBiography.
|
||||
type ArtistBiographyResponse struct {
|
||||
// Biography is the artist biography text.
|
||||
Biography string `json:"biography"`
|
||||
}
|
||||
|
||||
// SimilarArtistsRequest is the request for GetSimilarArtists.
|
||||
type SimilarArtistsRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
// Limit is the maximum number of similar artists to return.
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
// TopSongsResponse is the response for GetArtistTopSongs.
|
||||
type TopSongsResponse struct {
|
||||
// Songs is the list of top songs.
|
||||
Songs []SongRef `json:"songs"`
|
||||
}
|
||||
|
||||
// AlbumInfoResponse is the response for GetAlbumInfo.
|
||||
type AlbumInfoResponse struct {
|
||||
// Name is the album name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the album.
|
||||
MBID string `json:"mbid"`
|
||||
// Description is the album description/notes.
|
||||
Description string `json:"description"`
|
||||
// URL is the external URL for the album.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// ArtistRef is a reference to an artist with name and optional MBID.
|
||||
type ArtistRef struct {
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistURLResponse is the response for GetArtistURL.
|
||||
type ArtistURLResponse struct {
|
||||
// URL is the external URL for the artist.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// SimilarArtistsResponse is the response for GetSimilarArtists.
|
||||
type SimilarArtistsResponse struct {
|
||||
// Artists is the list of similar artists.
|
||||
Artists []ArtistRef `json:"artists"`
|
||||
}
|
||||
|
||||
// ArtistImagesResponse is the response for GetArtistImages.
|
||||
type ArtistImagesResponse struct {
|
||||
// Images is the list of artist images.
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// SongRef is a reference to a song with name and optional MBID.
|
||||
type SongRef struct {
|
||||
// Name is the song name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the song.
|
||||
MBID string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistMBIDRequest is the request for GetArtistMBID.
|
||||
type ArtistMBIDRequest struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ArtistMBIDResponse is the response for GetArtistMBID.
|
||||
type ArtistMBIDResponse struct {
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid"`
|
||||
}
|
||||
|
||||
// Metadata is the marker interface for metadata plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// MetadataAgent provides artist and album metadata retrieval.
|
||||
|
||||
@ -11,33 +11,20 @@ import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// ScrobblerErrorType indicates how Navidrome should handle scrobbler errors.
|
||||
type ScrobblerErrorType string
|
||||
// ScrobblerError represents an error type for scrobbling operations.
|
||||
type ScrobblerError string
|
||||
|
||||
const (
|
||||
// ScrobblerErrorNone indicates no error occurred.
|
||||
ScrobblerErrorNone ScrobblerErrorType = "none"
|
||||
// ScrobblerErrorNotAuthorized indicates the user is not authorized.
|
||||
ScrobblerErrorNotAuthorized ScrobblerErrorType = "not_authorized"
|
||||
ScrobblerErrorNotAuthorized ScrobblerError = "scrobbler(not_authorized)"
|
||||
// ScrobblerErrorRetryLater indicates the operation should be retried later.
|
||||
ScrobblerErrorRetryLater ScrobblerErrorType = "retry_later"
|
||||
ScrobblerErrorRetryLater ScrobblerError = "scrobbler(retry_later)"
|
||||
// ScrobblerErrorUnrecoverable indicates an unrecoverable error.
|
||||
ScrobblerErrorUnrecoverable ScrobblerErrorType = "unrecoverable"
|
||||
ScrobblerErrorUnrecoverable ScrobblerError = "scrobbler(unrecoverable)"
|
||||
)
|
||||
|
||||
// IsAuthorizedRequest is the request for authorization check.
|
||||
type IsAuthorizedRequest struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// IsAuthorizedResponse is the response for authorization check.
|
||||
type IsAuthorizedResponse struct {
|
||||
// Authorized indicates whether the user is authorized to scrobble.
|
||||
Authorized bool `json:"authorized"`
|
||||
}
|
||||
// Error implements the error interface for ScrobblerError.
|
||||
func (e ScrobblerError) Error() string { return string(e) }
|
||||
|
||||
// NowPlayingRequest is the request for now playing notification.
|
||||
type NowPlayingRequest struct {
|
||||
@ -51,14 +38,6 @@ type NowPlayingRequest struct {
|
||||
Position int32 `json:"position"`
|
||||
}
|
||||
|
||||
// ScrobblerResponse is the response for scrobbler operations.
|
||||
type ScrobblerResponse struct {
|
||||
// Error is the error message if the operation failed.
|
||||
Error string `json:"error,omitempty"`
|
||||
// ErrorType indicates how Navidrome should handle the error.
|
||||
ErrorType ScrobblerErrorType `json:"errorType,omitempty"`
|
||||
}
|
||||
|
||||
// ScrobbleRequest is the request for submitting a scrobble.
|
||||
type ScrobbleRequest struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
@ -103,6 +82,20 @@ type TrackInfo struct {
|
||||
MBZReleaseTrackID string `json:"mbzReleaseTrackId,omitempty"`
|
||||
}
|
||||
|
||||
// IsAuthorizedRequest is the request for authorization check.
|
||||
type IsAuthorizedRequest struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// IsAuthorizedResponse is the response for authorization check.
|
||||
type IsAuthorizedResponse struct {
|
||||
// Authorized indicates whether the user is authorized to scrobble.
|
||||
Authorized bool `json:"authorized"`
|
||||
}
|
||||
|
||||
// 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,
|
||||
@ -114,14 +107,14 @@ type Scrobbler interface {
|
||||
// IsAuthorized - IsAuthorized checks if a user is authorized to scrobble to this service.
|
||||
IsAuthorized(IsAuthorizedRequest) (*IsAuthorizedResponse, error)
|
||||
// NowPlaying - NowPlaying sends a now playing notification to the scrobbling service.
|
||||
NowPlaying(NowPlayingRequest) (*ScrobblerResponse, error)
|
||||
NowPlaying(NowPlayingRequest) error
|
||||
// Scrobble - Scrobble submits a completed scrobble to the scrobbling service.
|
||||
Scrobble(ScrobbleRequest) (*ScrobblerResponse, error)
|
||||
Scrobble(ScrobbleRequest) error
|
||||
} // Internal implementation holders
|
||||
var (
|
||||
isAuthorizedImpl func(IsAuthorizedRequest) (*IsAuthorizedResponse, error)
|
||||
nowPlayingImpl func(NowPlayingRequest) (*ScrobblerResponse, error)
|
||||
scrobbleImpl func(ScrobbleRequest) (*ScrobblerResponse, error)
|
||||
nowPlayingImpl func(NowPlayingRequest) error
|
||||
scrobbleImpl func(ScrobbleRequest) error
|
||||
)
|
||||
|
||||
// Register registers a scrobbler implementation.
|
||||
@ -176,13 +169,7 @@ func _NdScrobblerNowPlaying() int32 {
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := nowPlayingImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
if err := nowPlayingImpl(input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
@ -203,13 +190,7 @@ func _NdScrobblerScrobble() int32 {
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := scrobbleImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
if err := scrobbleImpl(input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
@ -8,33 +8,20 @@
|
||||
|
||||
package scrobbler
|
||||
|
||||
// ScrobblerErrorType indicates how Navidrome should handle scrobbler errors.
|
||||
type ScrobblerErrorType string
|
||||
// ScrobblerError represents an error type for scrobbling operations.
|
||||
type ScrobblerError string
|
||||
|
||||
const (
|
||||
// ScrobblerErrorNone indicates no error occurred.
|
||||
ScrobblerErrorNone ScrobblerErrorType = "none"
|
||||
// ScrobblerErrorNotAuthorized indicates the user is not authorized.
|
||||
ScrobblerErrorNotAuthorized ScrobblerErrorType = "not_authorized"
|
||||
ScrobblerErrorNotAuthorized ScrobblerError = "scrobbler(not_authorized)"
|
||||
// ScrobblerErrorRetryLater indicates the operation should be retried later.
|
||||
ScrobblerErrorRetryLater ScrobblerErrorType = "retry_later"
|
||||
ScrobblerErrorRetryLater ScrobblerError = "scrobbler(retry_later)"
|
||||
// ScrobblerErrorUnrecoverable indicates an unrecoverable error.
|
||||
ScrobblerErrorUnrecoverable ScrobblerErrorType = "unrecoverable"
|
||||
ScrobblerErrorUnrecoverable ScrobblerError = "scrobbler(unrecoverable)"
|
||||
)
|
||||
|
||||
// IsAuthorizedRequest is the request for authorization check.
|
||||
type IsAuthorizedRequest struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// IsAuthorizedResponse is the response for authorization check.
|
||||
type IsAuthorizedResponse struct {
|
||||
// Authorized indicates whether the user is authorized to scrobble.
|
||||
Authorized bool `json:"authorized"`
|
||||
}
|
||||
// Error implements the error interface for ScrobblerError.
|
||||
func (e ScrobblerError) Error() string { return string(e) }
|
||||
|
||||
// NowPlayingRequest is the request for now playing notification.
|
||||
type NowPlayingRequest struct {
|
||||
@ -48,14 +35,6 @@ type NowPlayingRequest struct {
|
||||
Position int32 `json:"position"`
|
||||
}
|
||||
|
||||
// ScrobblerResponse is the response for scrobbler operations.
|
||||
type ScrobblerResponse struct {
|
||||
// Error is the error message if the operation failed.
|
||||
Error string `json:"error,omitempty"`
|
||||
// ErrorType indicates how Navidrome should handle the error.
|
||||
ErrorType ScrobblerErrorType `json:"errorType,omitempty"`
|
||||
}
|
||||
|
||||
// ScrobbleRequest is the request for submitting a scrobble.
|
||||
type ScrobbleRequest struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
@ -100,6 +79,20 @@ type TrackInfo struct {
|
||||
MBZReleaseTrackID string `json:"mbzReleaseTrackId,omitempty"`
|
||||
}
|
||||
|
||||
// IsAuthorizedRequest is the request for authorization check.
|
||||
type IsAuthorizedRequest struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// IsAuthorizedResponse is the response for authorization check.
|
||||
type IsAuthorizedResponse struct {
|
||||
// Authorized indicates whether the user is authorized to scrobble.
|
||||
Authorized bool `json:"authorized"`
|
||||
}
|
||||
|
||||
// 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,
|
||||
@ -111,9 +104,9 @@ type Scrobbler interface {
|
||||
// IsAuthorized - IsAuthorized checks if a user is authorized to scrobble to this service.
|
||||
IsAuthorized(IsAuthorizedRequest) (*IsAuthorizedResponse, error)
|
||||
// NowPlaying - NowPlaying sends a now playing notification to the scrobbling service.
|
||||
NowPlaying(NowPlayingRequest) (*ScrobblerResponse, error)
|
||||
NowPlaying(NowPlayingRequest) error
|
||||
// Scrobble - Scrobble submits a completed scrobble to the scrobbling service.
|
||||
Scrobble(ScrobbleRequest) (*ScrobblerResponse, error)
|
||||
Scrobble(ScrobbleRequest) error
|
||||
}
|
||||
|
||||
// NotImplementedCode is the standard return code for unimplemented functions.
|
||||
|
||||
@ -11,6 +11,22 @@ import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// OnTextMessageRequest is the request provided when a text message is received.
|
||||
type OnTextMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Message is the text message content received from the WebSocket.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// OnBinaryMessageRequest is the request provided when a binary message is received.
|
||||
type OnBinaryMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// OnErrorRequest is the request provided when an error occurs on a WebSocket connection.
|
||||
type OnErrorRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection where the error occurred.
|
||||
@ -30,22 +46,6 @@ type OnCloseRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// OnTextMessageRequest is the request provided when a text message is received.
|
||||
type OnTextMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Message is the text message content received from the WebSocket.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// OnBinaryMessageRequest is the request provided when a binary message is received.
|
||||
type OnBinaryMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// WebSocket is the marker interface for websocket plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// WebSocketCallback provides WebSocket message handling.
|
||||
|
||||
@ -8,6 +8,22 @@
|
||||
|
||||
package websocket
|
||||
|
||||
// OnTextMessageRequest is the request provided when a text message is received.
|
||||
type OnTextMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Message is the text message content received from the WebSocket.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// OnBinaryMessageRequest is the request provided when a binary message is received.
|
||||
type OnBinaryMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// OnErrorRequest is the request provided when an error occurs on a WebSocket connection.
|
||||
type OnErrorRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection where the error occurred.
|
||||
@ -27,22 +43,6 @@ type OnCloseRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// OnTextMessageRequest is the request provided when a text message is received.
|
||||
type OnTextMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Message is the text message content received from the WebSocket.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// OnBinaryMessageRequest is the request provided when a binary message is received.
|
||||
type OnBinaryMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// WebSocket is the marker interface for websocket plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// WebSocketCallback provides WebSocket message handling.
|
||||
|
||||
@ -2,7 +2,7 @@ package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/core/scrobbler"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
@ -63,12 +63,8 @@ func (s *ScrobblerPlugin) NowPlaying(ctx context.Context, userId string, track *
|
||||
Position: int32(position),
|
||||
}
|
||||
|
||||
result, err := callPluginFunction[capabilities.NowPlayingRequest, *capabilities.ScrobblerResponse](ctx, s.plugin, FuncScrobblerNowPlaying, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapScrobblerError(result)
|
||||
err := callPluginFunctionNoOutput(ctx, s.plugin, FuncScrobblerNowPlaying, input)
|
||||
return mapScrobblerError(err)
|
||||
}
|
||||
|
||||
// Scrobble submits a scrobble to the scrobbler
|
||||
@ -81,12 +77,8 @@ func (s *ScrobblerPlugin) Scrobble(ctx context.Context, userId string, sc scrobb
|
||||
Timestamp: sc.TimeStamp.Unix(),
|
||||
}
|
||||
|
||||
result, err := callPluginFunction[capabilities.ScrobbleRequest, *capabilities.ScrobblerResponse](ctx, s.plugin, FuncScrobblerScrobble, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapScrobblerError(result)
|
||||
err := callPluginFunctionNoOutput(ctx, s.plugin, FuncScrobblerScrobble, input)
|
||||
return mapScrobblerError(err)
|
||||
}
|
||||
|
||||
// getUsernameFromContext extracts the username from the request context
|
||||
@ -117,34 +109,22 @@ func mediaFileToTrackInfo(mf *model.MediaFile) capabilities.TrackInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// mapScrobblerError converts the plugin output error to a scrobbler error
|
||||
func mapScrobblerError(output *capabilities.ScrobblerResponse) error {
|
||||
if output == nil {
|
||||
// mapScrobblerError converts plugin errors to scrobbler errors based on error message, as errors are returned as
|
||||
// strings from plugins.
|
||||
func mapScrobblerError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
switch output.ErrorType {
|
||||
case capabilities.ScrobblerErrorNone, "":
|
||||
return nil
|
||||
case capabilities.ScrobblerErrorNotAuthorized:
|
||||
if output.Error != "" {
|
||||
return fmt.Errorf("%w: %s", scrobbler.ErrNotAuthorized, output.Error)
|
||||
}
|
||||
errMsg := err.Error()
|
||||
switch {
|
||||
case strings.Contains(errMsg, capabilities.ScrobblerErrorNotAuthorized.Error()):
|
||||
return scrobbler.ErrNotAuthorized
|
||||
case capabilities.ScrobblerErrorRetryLater:
|
||||
if output.Error != "" {
|
||||
return fmt.Errorf("%w: %s", scrobbler.ErrRetryLater, output.Error)
|
||||
}
|
||||
case strings.Contains(errMsg, capabilities.ScrobblerErrorRetryLater.Error()):
|
||||
return scrobbler.ErrRetryLater
|
||||
case capabilities.ScrobblerErrorUnrecoverable:
|
||||
if output.Error != "" {
|
||||
return fmt.Errorf("%w: %s", scrobbler.ErrUnrecoverable, output.Error)
|
||||
}
|
||||
case strings.Contains(errMsg, capabilities.ScrobblerErrorUnrecoverable.Error()):
|
||||
return scrobbler.ErrUnrecoverable
|
||||
default:
|
||||
if output.Error != "" {
|
||||
return fmt.Errorf("unknown error type %q: %s", output.ErrorType, output.Error)
|
||||
}
|
||||
return fmt.Errorf("unknown error type: %s", output.ErrorType)
|
||||
return scrobbler.ErrUnrecoverable
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,12 +4,12 @@ package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/core/scrobbler"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/plugins/capabilities"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@ -88,7 +88,7 @@ var _ = Describe("ScrobblerPlugin", Ordered, func() {
|
||||
|
||||
It("returns error when plugin returns error", func() {
|
||||
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
|
||||
"test-scrobbler": {"error": "service unavailable", "error_type": "retry_later"},
|
||||
"test-scrobbler": {"error": "service unavailable", "error_type": "scrobbler(retry_later)"},
|
||||
}, "test-scrobbler"+PackageExtension)
|
||||
|
||||
sc, ok := manager.LoadScrobbler("test-scrobbler")
|
||||
@ -97,7 +97,7 @@ var _ = Describe("ScrobblerPlugin", Ordered, func() {
|
||||
track := &model.MediaFile{ID: "track-1", Title: "Test Song"}
|
||||
err := sc.NowPlaying(ctx, "user-1", track, 30)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(ContainSubstring("retry later")))
|
||||
Expect(err).To(MatchError(scrobbler.ErrRetryLater))
|
||||
})
|
||||
})
|
||||
|
||||
@ -123,7 +123,7 @@ var _ = Describe("ScrobblerPlugin", Ordered, func() {
|
||||
|
||||
It("returns error when plugin returns not_authorized error", func() {
|
||||
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
|
||||
"test-scrobbler": {"error": "user not linked", "error_type": "not_authorized"},
|
||||
"test-scrobbler": {"error": "user not linked", "error_type": "scrobbler(not_authorized)"},
|
||||
}, "test-scrobbler"+PackageExtension)
|
||||
|
||||
sc, ok := manager.LoadScrobbler("test-scrobbler")
|
||||
@ -135,12 +135,12 @@ var _ = Describe("ScrobblerPlugin", Ordered, func() {
|
||||
}
|
||||
err := sc.Scrobble(ctx, "user-1", scrobble)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(ContainSubstring("not authorized")))
|
||||
Expect(err).To(MatchError(scrobbler.ErrNotAuthorized))
|
||||
})
|
||||
|
||||
It("returns error when plugin returns unrecoverable error", func() {
|
||||
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
|
||||
"test-scrobbler": {"error": "track rejected", "error_type": "unrecoverable"},
|
||||
"test-scrobbler": {"error": "track rejected", "error_type": "scrobbler(unrecoverable)"},
|
||||
}, "test-scrobbler"+PackageExtension)
|
||||
|
||||
sc, ok := manager.LoadScrobbler("test-scrobbler")
|
||||
@ -152,7 +152,7 @@ var _ = Describe("ScrobblerPlugin", Ordered, func() {
|
||||
}
|
||||
err := sc.Scrobble(ctx, "user-1", scrobble)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(ContainSubstring("unrecoverable")))
|
||||
Expect(err).To(MatchError(scrobbler.ErrUnrecoverable))
|
||||
})
|
||||
})
|
||||
|
||||
@ -170,49 +170,27 @@ var _ = Describe("ScrobblerPlugin", Ordered, func() {
|
||||
})
|
||||
|
||||
var _ = Describe("mapScrobblerError", func() {
|
||||
It("returns nil for nil output", func() {
|
||||
It("returns nil for nil error", func() {
|
||||
Expect(mapScrobblerError(nil)).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns nil for empty error type", func() {
|
||||
output := &capabilities.ScrobblerResponse{ErrorType: ""}
|
||||
Expect(mapScrobblerError(output)).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns nil for 'none' error type", func() {
|
||||
output := &capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorNone}
|
||||
Expect(mapScrobblerError(output)).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns ErrNotAuthorized for 'not_authorized' error type", func() {
|
||||
output := &capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorNotAuthorized}
|
||||
err := mapScrobblerError(output)
|
||||
It("returns ErrNotAuthorized for error containing 'not_authorized'", func() {
|
||||
err := mapScrobblerError(errors.New("plugin error: scrobbler(not_authorized)"))
|
||||
Expect(err).To(MatchError(scrobbler.ErrNotAuthorized))
|
||||
})
|
||||
|
||||
It("returns ErrNotAuthorized with message", func() {
|
||||
output := &capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorNotAuthorized, Error: "user not linked"}
|
||||
err := mapScrobblerError(output)
|
||||
Expect(err).To(MatchError(ContainSubstring("not authorized")))
|
||||
Expect(err).To(MatchError(ContainSubstring("user not linked")))
|
||||
})
|
||||
|
||||
It("returns ErrRetryLater for 'retry_later' error type", func() {
|
||||
output := &capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorRetryLater}
|
||||
err := mapScrobblerError(output)
|
||||
It("returns ErrRetryLater for error containing 'retry_later'", func() {
|
||||
err := mapScrobblerError(errors.New("temporary failure: scrobbler(retry_later)"))
|
||||
Expect(err).To(MatchError(scrobbler.ErrRetryLater))
|
||||
})
|
||||
|
||||
It("returns ErrUnrecoverable for 'unrecoverable' error type", func() {
|
||||
output := &capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorUnrecoverable}
|
||||
err := mapScrobblerError(output)
|
||||
It("returns ErrUnrecoverable for error containing 'unrecoverable'", func() {
|
||||
err := mapScrobblerError(errors.New("fatal error: scrobbler(unrecoverable)"))
|
||||
Expect(err).To(MatchError(scrobbler.ErrUnrecoverable))
|
||||
})
|
||||
|
||||
It("returns error for unknown error type", func() {
|
||||
output := &capabilities.ScrobblerResponse{ErrorType: "unknown"}
|
||||
err := mapScrobblerError(output)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("unknown error type"))
|
||||
It("returns ErrUnrecoverable for unknown error", func() {
|
||||
err := mapScrobblerError(errors.New("some unknown error"))
|
||||
Expect(err).To(MatchError(scrobbler.ErrUnrecoverable))
|
||||
})
|
||||
})
|
||||
|
||||
9
plugins/testdata/test-scrobbler/go.mod
vendored
9
plugins/testdata/test-scrobbler/go.mod
vendored
@ -1,5 +1,10 @@
|
||||
module test-scrobbler
|
||||
|
||||
go 1.23
|
||||
go 1.25
|
||||
|
||||
require github.com/extism/go-pdk v1.1.3
|
||||
require (
|
||||
github.com/extism/go-pdk v1.1.3
|
||||
github.com/navidrome/navidrome v0.0.0
|
||||
)
|
||||
|
||||
replace github.com/navidrome/navidrome => ../../..
|
||||
|
||||
175
plugins/testdata/test-scrobbler/main.go
vendored
175
plugins/testdata/test-scrobbler/main.go
vendored
@ -3,70 +3,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/extism/go-pdk"
|
||||
pdk "github.com/extism/go-pdk"
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/scrobbler"
|
||||
)
|
||||
|
||||
// Scrobbler input/output types
|
||||
|
||||
type AuthInput struct {
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
func init() {
|
||||
scrobbler.Register(&testScrobbler{})
|
||||
}
|
||||
|
||||
type AuthOutput struct {
|
||||
Authorized bool `json:"authorized"`
|
||||
type testScrobbler struct{}
|
||||
|
||||
// IsAuthorized checks if a user is authorized.
|
||||
func (t *testScrobbler) IsAuthorized(scrobbler.IsAuthorizedRequest) (*scrobbler.IsAuthorizedResponse, error) {
|
||||
return &scrobbler.IsAuthorizedResponse{
|
||||
Authorized: checkAuthConfig(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type TrackInfo struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Album string `json:"album"`
|
||||
Artist string `json:"artist"`
|
||||
AlbumArtist string `json:"albumArtist"`
|
||||
Duration float32 `json:"duration"`
|
||||
TrackNumber int `json:"trackNumber"`
|
||||
DiscNumber int `json:"discNumber"`
|
||||
MbzRecordingID string `json:"mbzRecordingId,omitempty"`
|
||||
MbzAlbumID string `json:"mbzAlbumId,omitempty"`
|
||||
MbzArtistID string `json:"mbzArtistId,omitempty"`
|
||||
MbzReleaseGroupID string `json:"mbzReleaseGroupId,omitempty"`
|
||||
// NowPlaying sends a now playing notification.
|
||||
func (t *testScrobbler) NowPlaying(input scrobbler.NowPlayingRequest) error {
|
||||
// Check for configured error
|
||||
if err := checkConfigError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Log the now playing (for potential debugging)
|
||||
pdk.Log(pdk.LogInfo, "NowPlaying: "+input.Track.Title+" by "+input.Track.Artist)
|
||||
return nil
|
||||
}
|
||||
|
||||
type NowPlayingInput struct {
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
Track TrackInfo `json:"track"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
// Scrobble submits a scrobble.
|
||||
func (t *testScrobbler) Scrobble(input scrobbler.ScrobbleRequest) error {
|
||||
// Check for configured error
|
||||
if err := checkConfigError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type ScrobbleInput struct {
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
Track TrackInfo `json:"track"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// ScrobblerOutput contains error information from scrobble operations.
|
||||
// A nil pointer indicates success, non-nil indicates an error with details.
|
||||
type ScrobblerOutput struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorType string `json:"errorType,omitempty"`
|
||||
// Log the scrobble (for potential debugging)
|
||||
pdk.Log(pdk.LogInfo, "Scrobble: "+input.Track.Title+" by "+input.Track.Artist)
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkConfigError checks if the plugin is configured to return an error.
|
||||
// If "error" config is set, it returns the error message and error type.
|
||||
func checkConfigError() (bool, string, string) {
|
||||
// If "error" config is set, it returns the appropriate ScrobblerError.
|
||||
// Error types: "not_authorized", "retry_later", "unrecoverable"
|
||||
func checkConfigError() error {
|
||||
errMsg, hasErr := pdk.GetConfig("error")
|
||||
if !hasErr || errMsg == "" {
|
||||
return false, "", ""
|
||||
return nil
|
||||
}
|
||||
errType, _ := pdk.GetConfig("error_type")
|
||||
if errType == "" {
|
||||
errType = "unrecoverable"
|
||||
switch errType {
|
||||
case scrobbler.ScrobblerErrorNotAuthorized.Error():
|
||||
return fmt.Errorf("%w: %s", scrobbler.ScrobblerErrorNotAuthorized, errMsg)
|
||||
case scrobbler.ScrobblerErrorRetryLater.Error():
|
||||
return fmt.Errorf("%w: %s", scrobbler.ScrobblerErrorRetryLater, errMsg)
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", scrobbler.ScrobblerErrorUnrecoverable, errMsg)
|
||||
}
|
||||
return true, errMsg, errType
|
||||
}
|
||||
|
||||
// checkAuthConfig returns whether the plugin is configured to authorize users.
|
||||
@ -84,92 +81,4 @@ func checkAuthConfig() bool {
|
||||
return auth
|
||||
}
|
||||
|
||||
//go:wasmexport nd_scrobbler_is_authorized
|
||||
func ndScrobblerIsAuthorized() int32 {
|
||||
var input AuthInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
// Return pointer to output
|
||||
output := &AuthOutput{
|
||||
Authorized: checkAuthConfig(),
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//go:wasmexport nd_scrobbler_now_playing
|
||||
func ndScrobblerNowPlaying() int32 {
|
||||
var input NowPlayingInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
// Check for configured error - return pointer to error output
|
||||
hasErr, errMsg, errType := checkConfigError()
|
||||
if hasErr {
|
||||
output := &ScrobblerOutput{
|
||||
Error: errMsg,
|
||||
ErrorType: errType,
|
||||
}
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Log the now playing (for potential debugging)
|
||||
// In a real plugin, this would send to an external service
|
||||
pdk.Log(pdk.LogInfo, "NowPlaying: "+input.Track.Title+" by "+input.Track.Artist)
|
||||
|
||||
// Success - output nil (empty response)
|
||||
if err := pdk.OutputJSON(nil); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//go:wasmexport nd_scrobbler_scrobble
|
||||
func ndScrobblerScrobble() int32 {
|
||||
var input ScrobbleInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
// Check for configured error - return pointer to error output
|
||||
hasErr, errMsg, errType := checkConfigError()
|
||||
if hasErr {
|
||||
output := &ScrobblerOutput{
|
||||
Error: errMsg,
|
||||
ErrorType: errType,
|
||||
}
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Log the scrobble (for potential debugging)
|
||||
// In a real plugin, this would send to an external service
|
||||
pdk.Log(pdk.LogInfo, "Scrobble: "+input.Track.Title+" by "+input.Track.Artist)
|
||||
|
||||
// Success - output nil (empty response)
|
||||
if err := pdk.OutputJSON(nil); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func main() {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user