Initial commit
This commit is contained in:
commit
02a6dabcfb
62 files changed
+15751
No files matched your search
@@ -0,0 +1,400 @@
|
||||
package ynison
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/client"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/ynison/proto"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// YnisonClient represents a Ynison WebSocket client
|
||||
type YnisonClient struct {
|
||||
client *client.Client
|
||||
conn *websocket.Conn
|
||||
deviceID string
|
||||
sessionID int64
|
||||
mu sync.Mutex
|
||||
handlers map[string]func(*proto.PutYnisonStateResponse)
|
||||
ridCounter int64
|
||||
connected bool
|
||||
reconnectCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
wsURL string // For testing: override WebSocket URL
|
||||
closeOnce sync.Once // Ensures doneCh is closed only once
|
||||
}
|
||||
|
||||
// DeviceConfig holds device configuration for Ynison
|
||||
type DeviceConfig struct {
|
||||
DeviceID string
|
||||
Title string
|
||||
DeviceType proto.DeviceType
|
||||
AppName string
|
||||
AppVersion string
|
||||
CanBePlayer bool
|
||||
CanBeRemote bool
|
||||
VolumeGran int
|
||||
}
|
||||
|
||||
// NewYnisonClient creates a new Ynison client
|
||||
func NewYnisonClient(c *client.Client, config DeviceConfig) *YnisonClient {
|
||||
if config.DeviceID == "" {
|
||||
config.DeviceID = fmt.Sprintf("go-ynison-%d", time.Now().UnixNano())
|
||||
}
|
||||
if config.Title == "" {
|
||||
config.Title = "Go Ynison Client"
|
||||
}
|
||||
if config.AppName == "" {
|
||||
config.AppName = "YandexMusicGo"
|
||||
}
|
||||
if config.AppVersion == "" {
|
||||
config.AppVersion = "1.0.0"
|
||||
}
|
||||
if config.VolumeGran == 0 {
|
||||
config.VolumeGran = 100
|
||||
}
|
||||
|
||||
return &YnisonClient{
|
||||
client: c,
|
||||
deviceID: config.DeviceID,
|
||||
handlers: make(map[string]func(*proto.PutYnisonStateResponse)),
|
||||
reconnectCh: make(chan struct{}, 1),
|
||||
doneCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes a WebSocket connection to Ynison
|
||||
func (y *YnisonClient) Connect(ctx context.Context) error {
|
||||
token := y.client.GetToken()
|
||||
if token == "" {
|
||||
return fmt.Errorf("no auth token available")
|
||||
}
|
||||
|
||||
wsURL := y.wsURL
|
||||
if wsURL == "" {
|
||||
wsURL = fmt.Sprintf("wss://ynison.music.yandex.ru/redirector.YnisonRedirector/Connect?device_id=%s&token=%s", y.deviceID, token)
|
||||
}
|
||||
|
||||
dialer := websocket.DefaultDialer
|
||||
conn, _, err := dialer.DialContext(ctx, wsURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to Ynison: %w", err)
|
||||
}
|
||||
|
||||
y.conn = conn
|
||||
y.connected = true
|
||||
|
||||
// Start reading messages
|
||||
go y.readLoop(ctx)
|
||||
|
||||
// Send initial UpdateFullState
|
||||
return y.sendUpdateFullState(ctx)
|
||||
}
|
||||
|
||||
// overrideWSURL sets a custom WebSocket URL for testing
|
||||
func (y *YnisonClient) overrideWSURL(url string) {
|
||||
y.wsURL = url
|
||||
}
|
||||
|
||||
// Disconnect closes the WebSocket connection
|
||||
func (y *YnisonClient) Disconnect() error {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
|
||||
if y.conn != nil {
|
||||
y.connected = false
|
||||
y.closeOnce.Do(func() {
|
||||
close(y.doneCh)
|
||||
})
|
||||
err := y.conn.Close()
|
||||
y.conn = nil // Prevent double close
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnected returns true if the client is connected
|
||||
func (y *YnisonClient) IsConnected() bool {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
return y.connected
|
||||
}
|
||||
|
||||
// readLoop reads messages from the WebSocket connection
|
||||
func (y *YnisonClient) readLoop(ctx context.Context) {
|
||||
defer func() {
|
||||
y.mu.Lock()
|
||||
y.connected = false
|
||||
y.mu.Unlock()
|
||||
y.closeOnce.Do(func() {
|
||||
close(y.doneCh)
|
||||
})
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-y.doneCh:
|
||||
return
|
||||
default:
|
||||
_, message, err := y.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
fmt.Printf("Ynison read error: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var resp proto.PutYnisonStateResponse
|
||||
if err := json.Unmarshal(message, &resp); err != nil {
|
||||
fmt.Printf("Ynison unmarshal error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Call registered handlers
|
||||
y.mu.Lock()
|
||||
for _, handler := range y.handlers {
|
||||
go handler(&resp)
|
||||
}
|
||||
y.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendUpdateFullState sends the initial full state update
|
||||
func (y *YnisonClient) sendUpdateFullState(ctx context.Context) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdateFullState: &proto.UpdateFullState{
|
||||
PlayerState: &proto.PlayerState{
|
||||
Status: &proto.PlayingStatus{
|
||||
ProgressMs: 0,
|
||||
DurationMs: 0,
|
||||
Paused: true,
|
||||
PlaybackSpeed: 1.0,
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: y.deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
},
|
||||
PlayerQueue: &proto.PlayerQueue{
|
||||
EntityId: "",
|
||||
EntityType: proto.PlayerQueueEntityType_ENTITY_TYPE_UNSPECIFIED,
|
||||
CurrentPlayableIndex: -1,
|
||||
Options: &proto.PlayerStateOptions{
|
||||
RepeatMode: proto.PlayerStateOptionsRepeatMode_REPEAT_MODE_NONE,
|
||||
},
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: y.deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
EntityContext: proto.PlayerQueueEntityContext_ENTITY_CONTEXT_UNSPECIFIED,
|
||||
},
|
||||
PlayerQueueInjectOptional: &proto.PlayerQueueInject{
|
||||
PlayingStatus: &proto.PlayingStatus{
|
||||
ProgressMs: 0,
|
||||
DurationMs: 0,
|
||||
Paused: true,
|
||||
PlaybackSpeed: 1.0,
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: y.deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
},
|
||||
Playable: &proto.PlayerQueueInjectPlayable{
|
||||
PlayableId: "",
|
||||
PlayableType: proto.PlayerQueueInjectPlayablePlayableType_INJECT_PLAYABLE_TYPE_UNSPECIFIED,
|
||||
Title: "",
|
||||
},
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: y.deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
},
|
||||
},
|
||||
IsCurrentlyActive: false,
|
||||
Device: &proto.UpdateDevice{
|
||||
Info: &proto.DeviceInfo{
|
||||
DeviceId: y.deviceID,
|
||||
Title: "Go Ynison Client",
|
||||
Type: proto.DeviceType_DEVICE_TYPE_WEB_DESKTOP,
|
||||
AppName: "YandexMusicGo",
|
||||
AppVersion: "1.0.0",
|
||||
},
|
||||
Volume: 0.5,
|
||||
Capabilities: &proto.DeviceCapabilities{
|
||||
CanBePlayer: true,
|
||||
CanBeRemoteController: true,
|
||||
VolumeGranularity: 100,
|
||||
},
|
||||
VolumeInfo: &proto.DeviceVolume{
|
||||
Volume: 0.5,
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: y.deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
ActivityInterceptionType: proto.PutYnisonStateRequestActivityInterceptionType_INTERCEPTION_DO_NOT_INTERCEPT_BY_DEFAULT,
|
||||
}
|
||||
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// nextRid generates the next request ID
|
||||
func (y *YnisonClient) nextRid() string {
|
||||
y.ridCounter++
|
||||
return fmt.Sprintf("%s-%d", y.deviceID, y.ridCounter)
|
||||
}
|
||||
|
||||
// sendRequest sends a request to the Ynison server
|
||||
func (y *YnisonClient) sendRequest(ctx context.Context, req *proto.PutYnisonStateRequest) error {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
|
||||
if !y.connected || y.conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
return y.conn.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
// RegisterHandler registers a response handler
|
||||
func (y *YnisonClient) RegisterHandler(name string, handler func(*proto.PutYnisonStateResponse)) {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
y.handlers[name] = handler
|
||||
}
|
||||
|
||||
// UnregisterHandler removes a response handler
|
||||
func (y *YnisonClient) UnregisterHandler(name string) {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
delete(y.handlers, name)
|
||||
}
|
||||
|
||||
// UpdatePlayingStatus updates the playing status
|
||||
func (y *YnisonClient) UpdatePlayingStatus(ctx context.Context, status *proto.PlayingStatus) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdatePlayingStatus: &proto.UpdatePlayingStatus{
|
||||
PlayingStatus: status,
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateVolume updates the volume
|
||||
func (y *YnisonClient) UpdateVolume(ctx context.Context, deviceID string, volume float64, version *proto.UpdateVersion) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdateVolumeInfo: &proto.UpdateVolumeInfo{
|
||||
DeviceId: deviceID,
|
||||
VolumeInfo: &proto.DeviceVolume{
|
||||
Volume: volume,
|
||||
Version: version,
|
||||
},
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateActiveDevice updates the active device
|
||||
func (y *YnisonClient) UpdateActiveDevice(ctx context.Context, deviceID string) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdateActiveDevice: &proto.UpdateActiveDevice{
|
||||
DeviceIdOptional: deviceID,
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// UpdatePlayerState updates the player state (queue, repeat, shuffle)
|
||||
func (y *YnisonClient) UpdatePlayerState(ctx context.Context, state *proto.PlayerState) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdatePlayerState: &proto.UpdatePlayerState{
|
||||
PlayerState: state,
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// UpdatePlayerQueueInject updates the injected queue entity
|
||||
func (y *YnisonClient) UpdatePlayerQueueInject(ctx context.Context, inject *proto.PlayerQueueInject) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdatePlayerQueueInject: &proto.UpdatePlayerQueueInject{
|
||||
PlayerQueueInject: inject,
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// SyncStateFromEOV requests synchronization from EOV
|
||||
func (y *YnisonClient) SyncStateFromEOV(ctx context.Context, actualQueueID string) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
SyncStateFromEov: &proto.SyncStateFromEOV{
|
||||
ActualQueueId: actualQueueID,
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// GetDevices returns the list of connected devices from the last response
|
||||
func (y *YnisonClient) GetDevices() []*proto.Device {
|
||||
// This would need to be implemented with proper state tracking
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveDeviceID returns the active device ID from the last response
|
||||
func (y *YnisonClient) GetActiveDeviceID() string {
|
||||
// This would need to be implemented with proper state tracking
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetPlayerState returns the current player state from the last response
|
||||
func (y *YnisonClient) GetPlayerState() *proto.PlayerState {
|
||||
// This would need to be implemented with proper state tracking
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitForConnection waits for the connection to be established or context to be done
|
||||
func (y *YnisonClient) WaitForConnection(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-y.doneCh:
|
||||
return fmt.Errorf("connection closed")
|
||||
default:
|
||||
if y.IsConnected() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package ynison
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/client"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/ynison/proto"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// TestYnisonClient_Connect tests the Connect method with a mock WebSocket server
|
||||
func TestYnisonClient_Connect(t *testing.T) {
|
||||
// Create a mock WebSocket server
|
||||
var receivedMessages [][]byte
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
t.Logf("Upgrade error: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Read messages from client
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
t.Logf("Read error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
receivedMessages = append(receivedMessages, message)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Convert HTTP URL to WS URL
|
||||
wsURL := "ws" + server.URL[4:] // Replace http with ws
|
||||
|
||||
// Create a client with test token
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-123",
|
||||
Title: "Test Device",
|
||||
DeviceType: proto.DeviceType_DEVICE_TYPE_WEB_DESKTOP,
|
||||
AppName: "TestApp",
|
||||
AppVersion: "1.0.0",
|
||||
CanBePlayer: true,
|
||||
CanBeRemote: true,
|
||||
VolumeGran: 100,
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
// Override the wsURL for testing
|
||||
ynisonClient.overrideWSURL(wsURL)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := ynisonClient.Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Connect failed: %v", err)
|
||||
}
|
||||
|
||||
if !ynisonClient.IsConnected() {
|
||||
t.Error("Expected client to be connected after Connect")
|
||||
}
|
||||
|
||||
// Wait a bit for the initial message to be sent
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Verify that an UpdateFullState message was sent
|
||||
if len(receivedMessages) == 0 {
|
||||
t.Error("Expected at least one message to be sent")
|
||||
} else {
|
||||
var req proto.PutYnisonStateRequest
|
||||
if err := json.Unmarshal(receivedMessages[0], &req); err != nil {
|
||||
t.Fatalf("Failed to unmarshal sent message: %v", err)
|
||||
}
|
||||
if req.UpdateFullState == nil {
|
||||
t.Error("Expected UpdateFullState in first message")
|
||||
}
|
||||
if req.UpdateFullState.Device == nil {
|
||||
t.Error("Expected Device in UpdateFullState")
|
||||
}
|
||||
if req.UpdateFullState.Device.Info.DeviceId != "test-device-123" {
|
||||
t.Errorf("Expected DeviceId 'test-device-123', got '%s'", req.UpdateFullState.Device.Info.DeviceId)
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
err = ynisonClient.Disconnect()
|
||||
if err != nil {
|
||||
t.Errorf("Disconnect failed: %v", err)
|
||||
}
|
||||
|
||||
if ynisonClient.IsConnected() {
|
||||
t.Error("Expected client to be disconnected after Disconnect")
|
||||
}
|
||||
}
|
||||
|
||||
// TestYnisonClient_Disconnect tests the Disconnect method
|
||||
func TestYnisonClient_Disconnect(t *testing.T) {
|
||||
// Create a mock WebSocket server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Keep connection open until client closes it
|
||||
for {
|
||||
_, _, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
wsURL := "ws" + server.URL[4:]
|
||||
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-disconnect",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
ynisonClient.overrideWSURL(wsURL)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := ynisonClient.Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Connect failed: %v", err)
|
||||
}
|
||||
|
||||
if !ynisonClient.IsConnected() {
|
||||
t.Error("Expected client to be connected")
|
||||
}
|
||||
|
||||
err = ynisonClient.Disconnect()
|
||||
if err != nil {
|
||||
t.Errorf("Disconnect failed: %v", err)
|
||||
}
|
||||
|
||||
if ynisonClient.IsConnected() {
|
||||
t.Error("Expected client to be disconnected after Disconnect")
|
||||
}
|
||||
|
||||
// Calling Disconnect again should not error
|
||||
err = ynisonClient.Disconnect()
|
||||
if err != nil {
|
||||
t.Errorf("Second Disconnect should not error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestYnisonClient_UpdatePlayingStatus tests the UpdatePlayingStatus method
|
||||
func TestYnisonClient_UpdatePlayingStatus(t *testing.T) {
|
||||
var receivedMessages [][]byte
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
receivedMessages = append(receivedMessages, message)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
wsURL := "ws" + server.URL[4:]
|
||||
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-status",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
ynisonClient.overrideWSURL(wsURL)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := ynisonClient.Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Connect failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait for initial connection message
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
initialMsgCount := len(receivedMessages)
|
||||
|
||||
// Create a playing status
|
||||
status := CreatePlayingStatus(50000, 200000, false, 1.0, "test-device-status")
|
||||
|
||||
// Update playing status
|
||||
err = ynisonClient.UpdatePlayingStatus(ctx, status)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePlayingStatus failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait for message to be sent
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Verify the UpdatePlayingStatus message was sent
|
||||
if len(receivedMessages) <= initialMsgCount {
|
||||
t.Error("Expected UpdatePlayingStatus message to be sent")
|
||||
} else {
|
||||
var req proto.PutYnisonStateRequest
|
||||
if err := json.Unmarshal(receivedMessages[initialMsgCount], &req); err != nil {
|
||||
t.Fatalf("Failed to unmarshal sent message: %v", err)
|
||||
}
|
||||
if req.UpdatePlayingStatus == nil {
|
||||
t.Error("Expected UpdatePlayingStatus in message")
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus == nil {
|
||||
t.Error("Expected PlayingStatus in UpdatePlayingStatus")
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus.ProgressMs != 50000 {
|
||||
t.Errorf("Expected ProgressMs 50000, got %d", req.UpdatePlayingStatus.PlayingStatus.ProgressMs)
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus.DurationMs != 200000 {
|
||||
t.Errorf("Expected DurationMs 200000, got %d", req.UpdatePlayingStatus.PlayingStatus.DurationMs)
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus.Paused != false {
|
||||
t.Errorf("Expected Paused false, got %v", req.UpdatePlayingStatus.PlayingStatus.Paused)
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus.PlaybackSpeed != 1.0 {
|
||||
t.Errorf("Expected PlaybackSpeed 1.0, got %f", req.UpdatePlayingStatus.PlayingStatus.PlaybackSpeed)
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus.Version.DeviceId != "test-device-status" {
|
||||
t.Errorf("Expected DeviceId 'test-device-status', got '%s'", req.UpdatePlayingStatus.PlayingStatus.Version.DeviceId)
|
||||
}
|
||||
if req.Rid == "" {
|
||||
t.Error("Expected non-empty Rid")
|
||||
}
|
||||
}
|
||||
|
||||
err = ynisonClient.Disconnect()
|
||||
if err != nil {
|
||||
t.Errorf("Disconnect failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestYnisonClient_UpdatePlayingStatus_NotConnected tests UpdatePlayingStatus when not connected
|
||||
func TestYnisonClient_UpdatePlayingStatus_NotConnected(t *testing.T) {
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-not-connected",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
status := CreatePlayingStatus(50000, 200000, false, 1.0, "test-device-not-connected")
|
||||
|
||||
err := ynisonClient.UpdatePlayingStatus(context.Background(), status)
|
||||
if err == nil {
|
||||
t.Error("Expected error when not connected")
|
||||
}
|
||||
if err != nil && err.Error() != "not connected" {
|
||||
t.Errorf("Expected 'not connected' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestYnisonClient_Connect_NoToken tests Connect without a token
|
||||
func TestYnisonClient_Connect_NoToken(t *testing.T) {
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-no-token",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
err := ynisonClient.Connect(context.Background())
|
||||
if err == nil {
|
||||
t.Error("Expected error when no token")
|
||||
}
|
||||
if err != nil && err.Error() != "no auth token available" {
|
||||
t.Errorf("Expected 'no auth token available' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestYnisonClient_IsConnected tests the IsConnected method
|
||||
func TestYnisonClient_IsConnected(t *testing.T) {
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-isconnected",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
if ynisonClient.IsConnected() {
|
||||
t.Error("Expected client to not be connected initially")
|
||||
}
|
||||
|
||||
// We can't easily test connected state without a real server,
|
||||
// but we verified this in the Connect/Disconnect tests
|
||||
}
|
||||
|
||||
// TestYnisonClient_RegisterHandler tests handler registration
|
||||
func TestYnisonClient_RegisterHandler(t *testing.T) {
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-handler",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
ynisonClient.RegisterHandler("test", func(resp *proto.PutYnisonStateResponse) {})
|
||||
|
||||
// We can't easily test handler invocation without a real server,
|
||||
// but we can verify the handler is registered
|
||||
ynisonClient.UnregisterHandler("test")
|
||||
|
||||
// Re-register and verify it doesn't panic
|
||||
ynisonClient.RegisterHandler("test2", func(resp *proto.PutYnisonStateResponse) {})
|
||||
ynisonClient.UnregisterHandler("test2")
|
||||
ynisonClient.UnregisterHandler("nonexistent") // Should not panic
|
||||
}
|
||||
|
||||
// TestYnisonClient_DisconnectWithoutConnect tests Disconnect without prior Connect
|
||||
func TestYnisonClient_DisconnectWithoutConnect(t *testing.T) {
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-disconnect-no-connect",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
// Should not panic or error
|
||||
err := ynisonClient.Disconnect()
|
||||
if err != nil {
|
||||
t.Errorf("Disconnect without connect should not error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package ynison
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/ynison/proto"
|
||||
)
|
||||
|
||||
// CreatePlayingStatus creates a playing status
|
||||
func CreatePlayingStatus(progressMs, durationMs int64, paused bool, playbackSpeed float64, deviceID string) *proto.PlayingStatus {
|
||||
return &proto.PlayingStatus{
|
||||
ProgressMs: progressMs,
|
||||
DurationMs: durationMs,
|
||||
Paused: paused,
|
||||
PlaybackSpeed: playbackSpeed,
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePlayable creates a playable entity
|
||||
func CreatePlayable(playableID, albumID, from, title, coverURL string, playableType proto.PlayablePlayableType) *proto.Playable {
|
||||
return &proto.Playable{
|
||||
PlayableId: playableID,
|
||||
AlbumIdOptional: albumID,
|
||||
PlayableType: playableType,
|
||||
From: from,
|
||||
Title: title,
|
||||
CoverUrlOptional: coverURL,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTrackPlayable creates a track playable
|
||||
func CreateTrackPlayable(playableID, albumID, from, title, coverURL string, trackSourceKey uint32, batchID string) *proto.Playable {
|
||||
return &proto.Playable{
|
||||
PlayableId: playableID,
|
||||
AlbumIdOptional: albumID,
|
||||
PlayableType: proto.PlayablePlayableType_PLAYABLE_TYPE_TRACK,
|
||||
From: from,
|
||||
Title: title,
|
||||
CoverUrlOptional: coverURL,
|
||||
TrackInfo: &proto.TrackInfo{
|
||||
TrackSourceKey: trackSourceKey,
|
||||
BatchIdOptional: batchID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateVideoClipPlayable creates a video clip playable
|
||||
func CreateVideoClipPlayable(playableID, from, title, coverURL string, recommendationType proto.VideoClipInfoRecommendationType) *proto.Playable {
|
||||
return &proto.Playable{
|
||||
PlayableId: playableID,
|
||||
PlayableType: proto.PlayablePlayableType_PLAYABLE_TYPE_VIDEO_CLIP,
|
||||
From: from,
|
||||
Title: title,
|
||||
CoverUrlOptional: coverURL,
|
||||
VideoClipInfo: &proto.VideoClipInfo{
|
||||
RecommendationType: recommendationType,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePlayerQueue creates a player queue
|
||||
func CreatePlayerQueue(entityID string, entityType proto.PlayerQueueEntityType, playables []*proto.Playable, currentIndex int32, deviceID string) *proto.PlayerQueue {
|
||||
return &proto.PlayerQueue{
|
||||
EntityId: entityID,
|
||||
EntityType: entityType,
|
||||
CurrentPlayableIndex: currentIndex,
|
||||
PlayableList: playables,
|
||||
Options: &proto.PlayerStateOptions{
|
||||
RepeatMode: proto.PlayerStateOptionsRepeatMode_REPEAT_MODE_NONE,
|
||||
},
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
EntityContext: proto.PlayerQueueEntityContext_ENTITY_CONTEXT_BASED_ON_ENTITY_BY_DEFAULT,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWaveQueue creates a wave queue
|
||||
func CreateWaveQueue(recommended []*proto.Playable, liveIndex int32, sessionID string, deviceID string) *proto.PlayerQueueQueue {
|
||||
// Build track sources from playables
|
||||
var trackSources []*proto.PlayerQueueQueueWaveQueueEntityOptionsTrackSourceWithKey
|
||||
for i := range recommended {
|
||||
trackSources = append(trackSources, &proto.PlayerQueueQueueWaveQueueEntityOptionsTrackSourceWithKey{
|
||||
Key: uint32(i + 1),
|
||||
WaveSource: &proto.PlayerQueueQueueWaveQueueEntityOptionsWaveSource{
|
||||
SourceType: proto.WaveSourceSourceType_WAVE_SOURCE_ONLINE_BY_DEFAULT,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &proto.PlayerQueueQueue{
|
||||
WaveQueue: &proto.PlayerQueueQueueWaveQueue{
|
||||
RecommendedPlayableList: recommended,
|
||||
LivePlayableIndex: liveIndex,
|
||||
EntityOptions: &proto.PlayerQueueQueueWaveQueueEntityOptions{
|
||||
WaveEntityOptional: &proto.PlayerQueueQueueWaveQueueEntityOptionsWaveSession{
|
||||
SessionId: sessionID,
|
||||
},
|
||||
TrackSources: trackSources,
|
||||
},
|
||||
NavigationIdOptional: "",
|
||||
PlaybackActionIdOptional: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateGenerativeQueue creates a generative queue
|
||||
func CreateGenerativeQueue(id string) *proto.PlayerQueueQueue {
|
||||
return &proto.PlayerQueueQueue{
|
||||
GenerativeQueue: &proto.PlayerQueueQueueGenerativeQueue{
|
||||
Id: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFMRadioQueue creates an FM radio queue
|
||||
func CreateFMRadioQueue(id string) *proto.PlayerQueueQueue {
|
||||
return &proto.PlayerQueueQueue{
|
||||
FmRadioQueue: &proto.PlayerQueueQueueFmRadioQueue{
|
||||
Id: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateVideoWaveQueue creates a video wave queue
|
||||
func CreateVideoWaveQueue(id string) *proto.PlayerQueueQueue {
|
||||
return &proto.PlayerQueueQueue{
|
||||
VideoWaveQueue: &proto.PlayerQueueQueueVideoWaveQueue{
|
||||
Id: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateLocalTracksQueue creates a local tracks queue
|
||||
func CreateLocalTracksQueue() *proto.PlayerQueueQueue {
|
||||
return &proto.PlayerQueueQueue{
|
||||
LocalTracksQueue: &proto.PlayerQueueQueueLocalTracksQueue{},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDeviceInfo creates device info
|
||||
func CreateDeviceInfo(deviceID, title string, deviceType proto.DeviceType, appName, appVersion string) *proto.DeviceInfo {
|
||||
return &proto.DeviceInfo{
|
||||
DeviceId: deviceID,
|
||||
Title: title,
|
||||
Type: deviceType,
|
||||
AppName: appName,
|
||||
AppVersion: appVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDeviceCapabilities creates device capabilities
|
||||
func CreateDeviceCapabilities(canBePlayer, canBeRemote bool, volumeGranularity uint32) *proto.DeviceCapabilities {
|
||||
return &proto.DeviceCapabilities{
|
||||
CanBePlayer: canBePlayer,
|
||||
CanBeRemoteController: canBeRemote,
|
||||
VolumeGranularity: volumeGranularity,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDeviceVolume creates device volume
|
||||
func CreateDeviceVolume(volume float64, deviceID string) *proto.DeviceVolume {
|
||||
return &proto.DeviceVolume{
|
||||
Volume: volume,
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDevice creates a device
|
||||
func CreateDevice(deviceID, title string, deviceType proto.DeviceType, appName, appVersion string, volume float64) *proto.Device {
|
||||
return &proto.Device{
|
||||
Info: CreateDeviceInfo(deviceID, title, deviceType, appName, appVersion),
|
||||
Volume: volume,
|
||||
Capabilities: CreateDeviceCapabilities(true, true, 100),
|
||||
Session: &proto.Session{
|
||||
Id: time.Now().UnixNano(),
|
||||
},
|
||||
IsOffline: false,
|
||||
VolumeInfo: CreateDeviceVolume(volume, deviceID),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUpdateFullState creates an UpdateFullState request
|
||||
func CreateUpdateFullState(deviceID string, isActive bool, playerState *proto.PlayerState) *proto.UpdateFullState {
|
||||
return &proto.UpdateFullState{
|
||||
PlayerState: playerState,
|
||||
IsCurrentlyActive: isActive,
|
||||
Device: &proto.UpdateDevice{
|
||||
Info: CreateDeviceInfo(deviceID, "Go Ynison Client", proto.DeviceType_DEVICE_TYPE_WEB_DESKTOP, "YandexMusicGo", "1.0.0"),
|
||||
Volume: 0.5,
|
||||
Capabilities: CreateDeviceCapabilities(true, true, 100),
|
||||
VolumeInfo: CreateDeviceVolume(0.5, deviceID),
|
||||
},
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,406 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package ynison;
|
||||
|
||||
option go_package = "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/ynison/proto";
|
||||
|
||||
// DeviceType represents the type of device
|
||||
enum DeviceType {
|
||||
DEVICE_TYPE_UNSPECIFIED = 0;
|
||||
DEVICE_TYPE_WEB = 1;
|
||||
DEVICE_TYPE_ANDROID = 2;
|
||||
DEVICE_TYPE_IOS = 3;
|
||||
DEVICE_TYPE_SMART_SPEAKER = 4;
|
||||
DEVICE_TYPE_WEB_TV = 5;
|
||||
DEVICE_TYPE_ANDROID_TV = 6;
|
||||
DEVICE_TYPE_APPLE_TV = 7;
|
||||
DEVICE_TYPE_ANDROID_WEAR = 8;
|
||||
DEVICE_TYPE_WEB_DESKTOP = 9;
|
||||
}
|
||||
|
||||
// PlayablePlayableType represents the type of playable entity
|
||||
enum PlayablePlayableType {
|
||||
PLAYABLE_TYPE_UNSPECIFIED = 0;
|
||||
PLAYABLE_TYPE_TRACK = 1;
|
||||
PLAYABLE_TYPE_LOCAL_TRACK = 2;
|
||||
PLAYABLE_TYPE_INFINITE = 3;
|
||||
PLAYABLE_TYPE_VIDEO_CLIP = 4;
|
||||
}
|
||||
|
||||
// VideoClipInfoRecommendationType represents how a video clip got into the queue
|
||||
enum VideoClipInfoRecommendationType {
|
||||
VIDEO_CLIP_RECOMMENDATION_UNSPECIFIED = 0;
|
||||
VIDEO_CLIP_RECOMMENDATION_RECOMMENDED = 1;
|
||||
VIDEO_CLIP_RECOMMENDATION_ON_DEMAND = 2;
|
||||
VIDEO_CLIP_RECOMMENDATION_SEARCH = 3;
|
||||
VIDEO_CLIP_RECOMMENDATION_ARTIST = 4;
|
||||
VIDEO_CLIP_RECOMMENDATION_OWN = 5;
|
||||
VIDEO_CLIP_RECOMMENDATION_EDITORIAL_CHOICE = 6;
|
||||
}
|
||||
|
||||
// PlayerQueueEntityType represents the type of entity the queue originated from
|
||||
enum PlayerQueueEntityType {
|
||||
ENTITY_TYPE_UNSPECIFIED = 0;
|
||||
ENTITY_TYPE_ARTIST = 1;
|
||||
ENTITY_TYPE_PLAYLIST = 2;
|
||||
ENTITY_TYPE_ALBUM = 3;
|
||||
ENTITY_TYPE_RADIO = 4;
|
||||
ENTITY_TYPE_VARIOUS = 5;
|
||||
ENTITY_TYPE_GENERATIVE = 6;
|
||||
ENTITY_TYPE_FM_RADIO = 7;
|
||||
ENTITY_TYPE_VIDEO_WAVE = 8;
|
||||
ENTITY_TYPE_LOCAL_TRACKS = 9;
|
||||
}
|
||||
|
||||
// PlayerQueueEntityContext represents the playback context
|
||||
enum PlayerQueueEntityContext {
|
||||
ENTITY_CONTEXT_UNSPECIFIED = 0;
|
||||
ENTITY_CONTEXT_BASED_ON_ENTITY_BY_DEFAULT = 1;
|
||||
ENTITY_CONTEXT_USER_TRACKS = 2;
|
||||
ENTITY_CONTEXT_DOWNLOADED_TRACKS = 3;
|
||||
ENTITY_CONTEXT_SEARCH = 4;
|
||||
ENTITY_CONTEXT_MUSIC_HISTORY = 5;
|
||||
ENTITY_CONTEXT_MUSIC_HISTORY_SEARCH = 6;
|
||||
ENTITY_CONTEXT_ARTIST_MY_COLLECTION = 7;
|
||||
ENTITY_CONTEXT_ARTIST_FAMILIAR_FROM_WAVE = 8;
|
||||
}
|
||||
|
||||
// WaveSourceSourceType represents how a track got into the wave queue
|
||||
enum WaveSourceSourceType {
|
||||
WAVE_SOURCE_ONLINE_BY_DEFAULT = 0;
|
||||
WAVE_SOURCE_OFFLINE = 1;
|
||||
}
|
||||
|
||||
// PlayerStateOptionsRepeatMode represents the repeat mode
|
||||
enum PlayerStateOptionsRepeatMode {
|
||||
REPEAT_MODE_UNSPECIFIED = 0;
|
||||
REPEAT_MODE_NONE = 1;
|
||||
REPEAT_MODE_ONE = 2;
|
||||
REPEAT_MODE_ALL = 3;
|
||||
}
|
||||
|
||||
// PlayerQueueInjectPlayablePlayableType represents injectable playable type
|
||||
enum PlayerQueueInjectPlayablePlayableType {
|
||||
INJECT_PLAYABLE_TYPE_UNSPECIFIED = 0;
|
||||
INJECT_PLAYABLE_TYPE_ALICE_SHOT = 1;
|
||||
INJECT_PLAYABLE_TYPE_AD = 2;
|
||||
INJECT_PLAYABLE_TYPE_PREROLL = 3;
|
||||
}
|
||||
|
||||
// PutYnisonStateRequestActivityInterceptionType represents activity interception type
|
||||
enum PutYnisonStateRequestActivityInterceptionType {
|
||||
INTERCEPTION_DO_NOT_INTERCEPT_BY_DEFAULT = 0;
|
||||
INTERCEPTION_INTERCEPT_IF_NO_ONE_ACTIVE = 1;
|
||||
INTERCEPTION_INTERCEPT_EAGER = 2;
|
||||
}
|
||||
|
||||
// Playable represents a playable entity (track, video clip, etc.)
|
||||
message Playable {
|
||||
string playable_id = 1;
|
||||
string album_id_optional = 2;
|
||||
PlayablePlayableType playable_type = 3;
|
||||
string from = 4;
|
||||
string title = 5;
|
||||
string cover_url_optional = 6;
|
||||
VideoClipInfo video_clip_info = 7;
|
||||
TrackInfo track_info = 8;
|
||||
string navigation_id_optional = 9;
|
||||
string playback_action_id_optional = 10;
|
||||
}
|
||||
|
||||
// VideoClipInfo represents additional info about a video clip
|
||||
message VideoClipInfo {
|
||||
VideoClipInfoRecommendationType recommendation_type = 1;
|
||||
}
|
||||
|
||||
// TrackInfo represents additional info about a track
|
||||
message TrackInfo {
|
||||
uint32 track_source_key = 1;
|
||||
string batch_id_optional = 2;
|
||||
}
|
||||
|
||||
// UpdateVersion represents a version of changes
|
||||
message UpdateVersion {
|
||||
string device_id = 1;
|
||||
int64 version = 2;
|
||||
int64 timestamp_ms = 3;
|
||||
}
|
||||
|
||||
// PlayerQueue represents a playback queue
|
||||
message PlayerQueue {
|
||||
string entity_id = 1;
|
||||
PlayerQueueEntityType entity_type = 2;
|
||||
PlayerQueueQueue queue = 12;
|
||||
int32 current_playable_index = 3;
|
||||
repeated Playable playable_list = 4;
|
||||
PlayerStateOptions options = 5;
|
||||
UpdateVersion version = 6;
|
||||
Shuffle shuffle_optional = 7;
|
||||
PlayerQueueEntityContext entity_context = 8;
|
||||
string from_optional = 9;
|
||||
PlayerQueueInitialEntity initial_entity_optional = 10;
|
||||
PlayerQueuePlayerQueueOptions adding_options_optional = 11;
|
||||
string navigation_id_optional = 13;
|
||||
string filter_optional = 14;
|
||||
string playback_action_id_optional = 15;
|
||||
}
|
||||
|
||||
// PlayerQueueQueue represents hierarchical queue structure
|
||||
message PlayerQueueQueue {
|
||||
PlayerQueueQueueWaveQueue wave_queue = 1;
|
||||
PlayerQueueQueueGenerativeQueue generative_queue = 2;
|
||||
PlayerQueueQueueFmRadioQueue fm_radio_queue = 3;
|
||||
PlayerQueueQueueVideoWaveQueue video_wave_queue = 4;
|
||||
PlayerQueueQueueLocalTracksQueue local_tracks_queue = 5;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueue represents a wave queue
|
||||
message PlayerQueueQueueWaveQueue {
|
||||
repeated Playable recommended_playable_list = 1;
|
||||
int32 live_playable_index = 2;
|
||||
PlayerQueueQueueWaveQueueEntityOptions entity_options = 3;
|
||||
string navigation_id_optional = 4;
|
||||
string playback_action_id_optional = 5;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptions represents wave queue entity options
|
||||
message PlayerQueueQueueWaveQueueEntityOptions {
|
||||
PlayerQueueQueueWaveQueueEntityOptionsWaveSession wave_entity_optional = 1;
|
||||
repeated PlayerQueueQueueWaveQueueEntityOptionsTrackSourceWithKey track_sources = 2;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsWaveSession represents a wave session
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsWaveSession {
|
||||
string session_id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsTrackSourceWithKey represents a track source with key
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsTrackSourceWithKey {
|
||||
uint32 key = 1;
|
||||
PlayerQueueQueueWaveQueueEntityOptionsWaveSource wave_source = 2;
|
||||
PlayerQueueQueueWaveQueueEntityOptionsPhonotekaSource phonoteka_source = 3;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsWaveSource represents a wave source
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsWaveSource {
|
||||
WaveSourceSourceType source_type = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsPhonotekaSource represents a phonoteka source
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsPhonotekaSource {
|
||||
PlayerQueueEntityContext entity_context = 1;
|
||||
PlayerQueueQueueWaveQueueEntityOptionsArtistId artist_id = 2;
|
||||
PlayerQueueQueueWaveQueueEntityOptionsPlaylistId playlist_id = 3;
|
||||
PlayerQueueQueueWaveQueueEntityOptionsAlbumId album_id = 4;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsArtistId represents artist ID
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsArtistId {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsPlaylistId represents playlist ID
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsPlaylistId {
|
||||
string id = 1;
|
||||
string filter_optional = 2;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsAlbumId represents album ID
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsAlbumId {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueGenerativeQueue represents a generative queue
|
||||
message PlayerQueueQueueGenerativeQueue {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueFmRadioQueue represents an FM radio queue
|
||||
message PlayerQueueQueueFmRadioQueue {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueVideoWaveQueue represents a video wave queue
|
||||
message PlayerQueueQueueVideoWaveQueue {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueLocalTracksQueue represents a local tracks queue
|
||||
message PlayerQueueQueueLocalTracksQueue {}
|
||||
|
||||
// PlayerQueueInitialEntity represents initial queue entity (deprecated)
|
||||
message PlayerQueueInitialEntity {
|
||||
string entity_id = 1;
|
||||
PlayerQueueEntityType entity_type = 2;
|
||||
}
|
||||
|
||||
// PlayerQueuePlayerQueueOptions represents queue options (deprecated)
|
||||
message PlayerQueuePlayerQueueOptions {
|
||||
PlayerQueuePlayerQueueOptionsRadioOptions radio_options = 1;
|
||||
}
|
||||
|
||||
// PlayerQueuePlayerQueueOptionsRadioOptions represents radio options
|
||||
message PlayerQueuePlayerQueueOptionsRadioOptions {
|
||||
string session_id = 1;
|
||||
}
|
||||
|
||||
// PlayerStateOptions represents player state options
|
||||
message PlayerStateOptions {
|
||||
PlayerStateOptionsRepeatMode repeat_mode = 1;
|
||||
}
|
||||
|
||||
// Shuffle represents shuffle settings
|
||||
message Shuffle {
|
||||
repeated uint32 playable_indices = 1;
|
||||
}
|
||||
|
||||
// Device represents a device
|
||||
message Device {
|
||||
DeviceInfo info = 1;
|
||||
double volume = 2;
|
||||
DeviceCapabilities capabilities = 3;
|
||||
Session session = 4;
|
||||
bool is_offline = 5;
|
||||
DeviceVolume volume_info = 6;
|
||||
}
|
||||
|
||||
// DeviceVolume represents device volume state
|
||||
message DeviceVolume {
|
||||
double volume = 1;
|
||||
UpdateVersion version = 2;
|
||||
}
|
||||
|
||||
// DeviceInfo represents device information
|
||||
message DeviceInfo {
|
||||
string device_id = 1;
|
||||
string title = 2;
|
||||
DeviceType type = 3;
|
||||
string app_name = 4;
|
||||
string app_version = 5;
|
||||
}
|
||||
|
||||
// DeviceCapabilities represents device capabilities
|
||||
message DeviceCapabilities {
|
||||
bool can_be_player = 1;
|
||||
bool can_be_remote_controller = 2;
|
||||
uint32 volume_granularity = 3;
|
||||
}
|
||||
|
||||
// Session represents a device session
|
||||
message Session {
|
||||
int64 id = 1;
|
||||
}
|
||||
|
||||
// PlayingStatus represents playback status
|
||||
message PlayingStatus {
|
||||
int64 progress_ms = 1;
|
||||
int64 duration_ms = 2;
|
||||
bool paused = 3;
|
||||
double playback_speed = 4;
|
||||
UpdateVersion version = 5;
|
||||
}
|
||||
|
||||
// PlayerQueueInject represents injected queue entity state
|
||||
message PlayerQueueInject {
|
||||
PlayingStatus playing_status = 1;
|
||||
PlayerQueueInjectPlayable playable = 2;
|
||||
UpdateVersion version = 3;
|
||||
}
|
||||
|
||||
// PlayerQueueInjectPlayable represents an injectable playable
|
||||
message PlayerQueueInjectPlayable {
|
||||
string playable_id = 1;
|
||||
PlayerQueueInjectPlayablePlayableType playable_type = 2;
|
||||
string title = 3;
|
||||
string cover_url = 4;
|
||||
}
|
||||
|
||||
// PlayerState represents player state
|
||||
message PlayerState {
|
||||
PlayingStatus status = 1;
|
||||
PlayerQueue player_queue = 2;
|
||||
PlayerQueueInject player_queue_inject_optional = 3;
|
||||
}
|
||||
|
||||
// PutYnisonStateRequest represents a state update request
|
||||
message PutYnisonStateRequest {
|
||||
UpdateFullState update_full_state = 1;
|
||||
UpdateActiveDevice update_active_device = 2;
|
||||
UpdatePlayingStatus update_playing_status = 3;
|
||||
UpdatePlayerState update_player_state = 4;
|
||||
UpdateVolume update_volume = 5;
|
||||
UpdatePlayerQueueInject update_player_queue_inject = 6;
|
||||
UpdateSessionParams update_session_params = 9;
|
||||
UpdateVolumeInfo update_volume_info = 11;
|
||||
SyncStateFromEOV sync_state_from_eov = 12;
|
||||
int64 player_action_timestamp_ms = 7;
|
||||
string rid = 8;
|
||||
PutYnisonStateRequestActivityInterceptionType activity_interception_type = 10;
|
||||
}
|
||||
|
||||
// PutYnisonStateResponse represents a state update response
|
||||
message PutYnisonStateResponse {
|
||||
PlayerState player_state = 1;
|
||||
repeated Device devices = 2;
|
||||
string active_device_id_optional = 3;
|
||||
int64 timestamp_ms = 4;
|
||||
string rid = 5;
|
||||
}
|
||||
|
||||
// UpdatePlayerQueueInject represents player queue inject update
|
||||
message UpdatePlayerQueueInject {
|
||||
PlayerQueueInject player_queue_inject = 1;
|
||||
}
|
||||
|
||||
// UpdateActiveDevice represents active device update
|
||||
message UpdateActiveDevice {
|
||||
string device_id_optional = 1;
|
||||
}
|
||||
|
||||
// UpdatePlayingStatus represents playing status update
|
||||
message UpdatePlayingStatus {
|
||||
PlayingStatus playing_status = 1;
|
||||
}
|
||||
|
||||
// UpdateVolume represents volume update (deprecated)
|
||||
message UpdateVolume {
|
||||
double volume = 1;
|
||||
string device_id = 2;
|
||||
}
|
||||
|
||||
// UpdateVolumeInfo represents volume info update
|
||||
message UpdateVolumeInfo {
|
||||
string device_id = 1;
|
||||
DeviceVolume volume_info = 2;
|
||||
}
|
||||
|
||||
// UpdatePlayerState represents player state update
|
||||
message UpdatePlayerState {
|
||||
PlayerState player_state = 1;
|
||||
}
|
||||
|
||||
// UpdateFullState represents full state update
|
||||
message UpdateFullState {
|
||||
PlayerState player_state = 1;
|
||||
bool is_currently_active = 2;
|
||||
UpdateDevice device = 3;
|
||||
SyncStateFromEOV sync_state_from_eov_optional = 4;
|
||||
}
|
||||
|
||||
// UpdateSessionParams represents session params update
|
||||
message UpdateSessionParams {
|
||||
bool mute_events_if_passive = 1;
|
||||
}
|
||||
|
||||
// UpdateDevice represents device update
|
||||
message UpdateDevice {
|
||||
DeviceInfo info = 1;
|
||||
double volume = 2;
|
||||
DeviceCapabilities capabilities = 3;
|
||||
DeviceVolume volume_info = 4;
|
||||
}
|
||||
|
||||
// SyncStateFromEOV represents sync state from EOV request
|
||||
message SyncStateFromEOV {
|
||||
string actual_queue_id = 1;
|
||||
}
|
||||
Reference in new issue
Block a user