Initial commit

This commit is contained in:
Mrixs committed 2026-10-14 23:04:51 +03:00
commit 02a6dabcfb
62 files changed
+15751

No files matched your search

+400
View File
@@ -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")
}
}
+383
View File
@@ -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)
}
}
+206
View File
@@ -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
+406
View File
@@ -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;
}