update
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
This commit is contained in:
304
plugins/examples/crypto-ticker/plugin.go
Normal file
304
plugins/examples/crypto-ticker/plugin.go
Normal file
@@ -0,0 +1,304 @@
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/plugins/api"
|
||||
"github.com/navidrome/navidrome/plugins/host/config"
|
||||
"github.com/navidrome/navidrome/plugins/host/scheduler"
|
||||
"github.com/navidrome/navidrome/plugins/host/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Coinbase WebSocket API endpoint
|
||||
coinbaseWSEndpoint = "wss://ws-feed.exchange.coinbase.com"
|
||||
|
||||
// Connection ID for our WebSocket connection
|
||||
connectionID = "crypto-ticker-connection"
|
||||
|
||||
// ID for the reconnection schedule
|
||||
reconnectScheduleID = "crypto-ticker-reconnect"
|
||||
)
|
||||
|
||||
var (
|
||||
// Store ticker symbols from the configuration
|
||||
tickers []string
|
||||
)
|
||||
|
||||
// WebSocketService instance used to manage WebSocket connections and communication.
|
||||
var wsService = websocket.NewWebSocketService()
|
||||
|
||||
// ConfigService instance for accessing plugin configuration.
|
||||
var configService = config.NewConfigService()
|
||||
|
||||
// SchedulerService instance for scheduling tasks.
|
||||
var schedService = scheduler.NewSchedulerService()
|
||||
|
||||
// CryptoTickerPlugin implements WebSocketCallback, LifecycleManagement, and SchedulerCallback interfaces
|
||||
type CryptoTickerPlugin struct{}
|
||||
|
||||
// Coinbase subscription message structure
|
||||
type CoinbaseSubscription struct {
|
||||
Type string `json:"type"`
|
||||
ProductIDs []string `json:"product_ids"`
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
// Coinbase ticker message structure
|
||||
type CoinbaseTicker struct {
|
||||
Type string `json:"type"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
ProductID string `json:"product_id"`
|
||||
Price string `json:"price"`
|
||||
Open24h string `json:"open_24h"`
|
||||
Volume24h string `json:"volume_24h"`
|
||||
Low24h string `json:"low_24h"`
|
||||
High24h string `json:"high_24h"`
|
||||
Volume30d string `json:"volume_30d"`
|
||||
BestBid string `json:"best_bid"`
|
||||
BestAsk string `json:"best_ask"`
|
||||
Side string `json:"side"`
|
||||
Time string `json:"time"`
|
||||
TradeID int `json:"trade_id"`
|
||||
LastSize string `json:"last_size"`
|
||||
}
|
||||
|
||||
// OnInit is called when the plugin is loaded
|
||||
func (CryptoTickerPlugin) OnInit(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) {
|
||||
log.Printf("Crypto Ticker Plugin initializing...")
|
||||
|
||||
// Check if ticker configuration exists
|
||||
tickerConfig, ok := req.Config["tickers"]
|
||||
if !ok {
|
||||
return &api.InitResponse{Error: "Missing 'tickers' configuration"}, nil
|
||||
}
|
||||
|
||||
// Parse ticker symbols
|
||||
tickers := parseTickerSymbols(tickerConfig)
|
||||
log.Printf("Configured tickers: %v", tickers)
|
||||
|
||||
// Connect to WebSocket and subscribe to tickers
|
||||
err := connectAndSubscribe(ctx, tickers)
|
||||
if err != nil {
|
||||
return &api.InitResponse{Error: err.Error()}, nil
|
||||
}
|
||||
|
||||
return &api.InitResponse{}, nil
|
||||
}
|
||||
|
||||
// Helper function to parse ticker symbols from a comma-separated string
|
||||
func parseTickerSymbols(tickerConfig string) []string {
|
||||
tickers := strings.Split(tickerConfig, ",")
|
||||
for i, ticker := range tickers {
|
||||
tickers[i] = strings.TrimSpace(ticker)
|
||||
|
||||
// Add -USD suffix if not present
|
||||
if !strings.Contains(tickers[i], "-") {
|
||||
tickers[i] = tickers[i] + "-USD"
|
||||
}
|
||||
}
|
||||
return tickers
|
||||
}
|
||||
|
||||
// Helper function to connect to WebSocket and subscribe to tickers
|
||||
func connectAndSubscribe(ctx context.Context, tickers []string) error {
|
||||
// Connect to the WebSocket API
|
||||
_, err := wsService.Connect(ctx, &websocket.ConnectRequest{
|
||||
Url: coinbaseWSEndpoint,
|
||||
ConnectionId: connectionID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to connect to Coinbase WebSocket API: %v", err)
|
||||
return fmt.Errorf("WebSocket connection error: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Connected to Coinbase WebSocket API")
|
||||
|
||||
// Subscribe to ticker channel for the configured symbols
|
||||
subscription := CoinbaseSubscription{
|
||||
Type: "subscribe",
|
||||
ProductIDs: tickers,
|
||||
Channels: []string{"ticker"},
|
||||
}
|
||||
|
||||
subscriptionJSON, err := json.Marshal(subscription)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal subscription message: %v", err)
|
||||
return fmt.Errorf("JSON marshal error: %v", err)
|
||||
}
|
||||
|
||||
// Send subscription message
|
||||
_, err = wsService.SendText(ctx, &websocket.SendTextRequest{
|
||||
ConnectionId: connectionID,
|
||||
Message: string(subscriptionJSON),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to send subscription message: %v", err)
|
||||
return fmt.Errorf("WebSocket send error: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Subscription message sent to Coinbase WebSocket API")
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnTextMessage is called when a text message is received from the WebSocket
|
||||
func (CryptoTickerPlugin) OnTextMessage(ctx context.Context, req *api.OnTextMessageRequest) (*api.OnTextMessageResponse, error) {
|
||||
// Only process messages from our connection
|
||||
if req.ConnectionId != connectionID {
|
||||
log.Printf("Received message from unexpected connection: %s", req.ConnectionId)
|
||||
return &api.OnTextMessageResponse{}, nil
|
||||
}
|
||||
|
||||
// Try to parse as a ticker message
|
||||
var ticker CoinbaseTicker
|
||||
err := json.Unmarshal([]byte(req.Message), &ticker)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse ticker message: %v", err)
|
||||
return &api.OnTextMessageResponse{}, nil
|
||||
}
|
||||
|
||||
// If the message is not a ticker or has an error, just log it
|
||||
if ticker.Type != "ticker" {
|
||||
// This could be subscription confirmation or other messages
|
||||
log.Printf("Received non-ticker message: %s", req.Message)
|
||||
return &api.OnTextMessageResponse{}, nil
|
||||
}
|
||||
|
||||
// Format and print ticker information
|
||||
log.Printf("CRYPTO TICKER: %s Price: %s Best Bid: %s Best Ask: %s 24h Change: %s%%\n",
|
||||
ticker.ProductID,
|
||||
ticker.Price,
|
||||
ticker.BestBid,
|
||||
ticker.BestAsk,
|
||||
calculatePercentChange(ticker.Open24h, ticker.Price),
|
||||
)
|
||||
|
||||
return &api.OnTextMessageResponse{}, nil
|
||||
}
|
||||
|
||||
// OnBinaryMessage is called when a binary message is received
|
||||
func (CryptoTickerPlugin) OnBinaryMessage(ctx context.Context, req *api.OnBinaryMessageRequest) (*api.OnBinaryMessageResponse, error) {
|
||||
// Not expected from Coinbase WebSocket API
|
||||
return &api.OnBinaryMessageResponse{}, nil
|
||||
}
|
||||
|
||||
// OnError is called when an error occurs on the WebSocket connection
|
||||
func (CryptoTickerPlugin) OnError(ctx context.Context, req *api.OnErrorRequest) (*api.OnErrorResponse, error) {
|
||||
log.Printf("WebSocket error: %s", req.Error)
|
||||
return &api.OnErrorResponse{}, nil
|
||||
}
|
||||
|
||||
// OnClose is called when the WebSocket connection is closed
|
||||
func (CryptoTickerPlugin) OnClose(ctx context.Context, req *api.OnCloseRequest) (*api.OnCloseResponse, error) {
|
||||
log.Printf("WebSocket connection closed with code %d: %s", req.Code, req.Reason)
|
||||
|
||||
// Try to reconnect if this is our connection
|
||||
if req.ConnectionId == connectionID {
|
||||
log.Printf("Scheduling reconnection attempts every 2 seconds...")
|
||||
|
||||
// Create a recurring schedule to attempt reconnection every 2 seconds
|
||||
resp, err := schedService.ScheduleRecurring(ctx, &scheduler.ScheduleRecurringRequest{
|
||||
// Run every 2 seconds using cron expression
|
||||
CronExpression: "*/2 * * * * *",
|
||||
ScheduleId: reconnectScheduleID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to schedule reconnection attempts: %v", err)
|
||||
} else {
|
||||
log.Printf("Reconnection schedule created with ID: %s", resp.ScheduleId)
|
||||
}
|
||||
}
|
||||
|
||||
return &api.OnCloseResponse{}, nil
|
||||
}
|
||||
|
||||
// OnSchedulerCallback is called when a scheduled event triggers
|
||||
func (CryptoTickerPlugin) OnSchedulerCallback(ctx context.Context, req *api.SchedulerCallbackRequest) (*api.SchedulerCallbackResponse, error) {
|
||||
// Only handle our reconnection schedule
|
||||
if req.ScheduleId != reconnectScheduleID {
|
||||
log.Printf("Received callback for unknown schedule: %s", req.ScheduleId)
|
||||
return &api.SchedulerCallbackResponse{}, nil
|
||||
}
|
||||
|
||||
log.Printf("Attempting to reconnect to Coinbase WebSocket API...")
|
||||
|
||||
// Get the current ticker configuration
|
||||
configResp, err := configService.GetPluginConfig(ctx, &config.GetPluginConfigRequest{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to get plugin configuration: %v", err)
|
||||
return &api.SchedulerCallbackResponse{Error: fmt.Sprintf("Config error: %v", err)}, nil
|
||||
}
|
||||
|
||||
// Check if ticker configuration exists
|
||||
tickerConfig, ok := configResp.Config["tickers"]
|
||||
if !ok {
|
||||
log.Printf("Missing 'tickers' configuration")
|
||||
return &api.SchedulerCallbackResponse{Error: "Missing 'tickers' configuration"}, nil
|
||||
}
|
||||
|
||||
// Parse ticker symbols
|
||||
tickers := parseTickerSymbols(tickerConfig)
|
||||
log.Printf("Reconnecting with tickers: %v", tickers)
|
||||
|
||||
// Try to connect and subscribe
|
||||
err = connectAndSubscribe(ctx, tickers)
|
||||
if err != nil {
|
||||
log.Printf("Reconnection attempt failed: %v", err)
|
||||
return &api.SchedulerCallbackResponse{Error: err.Error()}, nil
|
||||
}
|
||||
|
||||
// Successfully reconnected, cancel the reconnection schedule
|
||||
_, err = schedService.CancelSchedule(ctx, &scheduler.CancelRequest{
|
||||
ScheduleId: reconnectScheduleID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to cancel reconnection schedule: %v", err)
|
||||
} else {
|
||||
log.Printf("Reconnection schedule canceled after successful reconnection")
|
||||
}
|
||||
|
||||
return &api.SchedulerCallbackResponse{}, nil
|
||||
}
|
||||
|
||||
// Helper function to calculate percent change
|
||||
func calculatePercentChange(open, current string) string {
|
||||
var openFloat, currentFloat float64
|
||||
_, err := fmt.Sscanf(open, "%f", &openFloat)
|
||||
if err != nil {
|
||||
return "N/A"
|
||||
}
|
||||
_, err = fmt.Sscanf(current, "%f", ¤tFloat)
|
||||
if err != nil {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
if openFloat == 0 {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
change := ((currentFloat - openFloat) / openFloat) * 100
|
||||
return fmt.Sprintf("%.2f", change)
|
||||
}
|
||||
|
||||
// Required by Go WASI build
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
// Configure logging: No timestamps, no source file/line, prepend [Crypto]
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("[Crypto] ")
|
||||
|
||||
api.RegisterWebSocketCallback(CryptoTickerPlugin{})
|
||||
api.RegisterLifecycleManagement(CryptoTickerPlugin{})
|
||||
api.RegisterSchedulerCallback(CryptoTickerPlugin{})
|
||||
}
|
||||
Reference in New Issue
Block a user