From 9b3baebda110755b58c767bada4681c0fb6ff0c3 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 22 Aug 2018 18:44:20 -0400 Subject: [PATCH] feature: add player --- lavalink.go | 22 +++++++--- model.go | 43 ++++++++++++++++++ node.go | 41 +++++++++++++++++ player.go | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 player.go diff --git a/lavalink.go b/lavalink.go index b85de12..0015124 100644 --- a/lavalink.go +++ b/lavalink.go @@ -11,14 +11,17 @@ type Lavalink struct { // UserID is the Discord User ID of the bot UserID int - nodes []Node + nodes []Node + players map[string]*Player } var ( - errNoNodes = errors.New("No nodes present") - errNodeNotFound = errors.New("Couldn't find that node") - errInvalidVersion = errors.New("This library requires Lavalink >= 3") - errUnknownPayload = errors.New("Lavalink sent an unknown payload") + errNoNodes = errors.New("No nodes present") + errNodeNotFound = errors.New("Couldn't find that node") + errPlayerNotFound = errors.New("Couldn't find a player for that guild") + errVolumeOutOfRange = errors.New("Volume is out of range, must be within [0, 1000]") + errInvalidVersion = errors.New("This library requires Lavalink >= 3") + errUnknownPayload = errors.New("Lavalink sent an unknown payload") ) // NewLavalink creates a new Lavalink manager @@ -74,3 +77,12 @@ func (lavalink *Lavalink) BestNode() (*Node, error) { // TODO: lookup latency return &lavalink.nodes[0], nil } + +// GetPlayer gets a player for a guild +func (lavalink *Lavalink) GetPlayer(guild string) (*Player, error) { + p, ok := lavalink.players[guild] + if !ok { + return nil, errPlayerNotFound + } + return p, nil +} diff --git a/model.go b/model.go index 7adef7e..e881001 100644 --- a/model.go +++ b/model.go @@ -51,3 +51,46 @@ type TrackInfo struct { Length int `json:"length"` Position int `json:"position"` } + +const ( + opVoiceUpdate = "voiceUpdate" + opPlay = "play" + opStop = "stop" + opPause = "pause" + opSeek = "seek" + opVolume = "volume" + opDestroy = "destroy" + opPlayerUpdate = "playerUpdate" + opEvent = "event" +) + +type message struct { + Op string `json:"op"` + GuildID string `json:"guildId,omitempty"` + SessionID string `json:"sessionId,omitempty"` + Event *VoiceServerUpdate `json:"event,omitempty"` + Track string `json:"track,omitempty"` + StartTime string `json:"startTime,omitempty"` + EndTime string `json:"endTime,omitempty"` + Pause bool `json:"pause,omitempty"` + Position int `json:"position,omitempty"` + Volume int `json:"volume,omitempty"` + State *state `json:"state,omitempty"` + Type string `json:"type,omitempty"` + Reason string `json:"reason,omitempty"` + Error string `json:"error,omitempty"` + ThresholdMs int `json:"thresholdMs,omitempty"` + // TODO: stats +} + +type state struct { + Time int `json:"time"` + Position int `json:"position"` +} + +// VoiceServerUpdate is a raw Discord VOICE_SERVER_UPDATE event +type VoiceServerUpdate struct { + GuildID int `json:"guild_id"` + Endpoint string `json:"endpoint"` + Token string `json:"token"` +} diff --git a/node.go b/node.go index bfbd1d1..79048be 100644 --- a/node.go +++ b/node.go @@ -93,9 +93,50 @@ func (node *Node) onEvent(msgType int, msg []byte) error { if msgType != websocket.TextMessage { return errUnknownPayload } + + m := message{} + err := json.Unmarshal(msg, &m) + if err != nil { + return err + } + + switch m.Op { + case opPlayerUpdate: + // todo + case opEvent: + // todo + default: + return errUnknownPayload + } + return nil } +// CreatePlayer creates an audio player on this node +func (node *Node) CreatePlayer(guildID string, sessionID string, event VoiceServerUpdate) (*Player, error) { + msg := message{ + Op: opVoiceUpdate, + GuildID: guildID, + SessionID: sessionID, + Event: &event, + } + data, err := json.Marshal(msg) + if err != nil { + return nil, err + } + err = node.wsConn.WriteMessage(websocket.TextMessage, data) + if err != nil { + return nil, err + } + player := &Player{ + guildID: guildID, + manager: node.manager, + node: node, + } + node.manager.players[guildID] = player + return player, nil +} + // LoadTracks queries lavalink to return a Tracks object // // query should be a valid Lavaplayer query, including but not limited to: diff --git a/player.go b/player.go new file mode 100644 index 0000000..5e62cb5 --- /dev/null +++ b/player.go @@ -0,0 +1,124 @@ +package gavalink + +import ( + "encoding/json" + "strconv" + + "github.com/gorilla/websocket" +) + +// Player is a Lavalink player +type Player struct { + guildID string + manager *Lavalink + node *Node +} + +// Play will play the given track completely +func (player *Player) Play(track string) error { + return player.PlayAt(track, 0, 0) +} + +// PlayAt will play the given track at the specified start and end times +// +// Setting a time to 0 will omit it. +func (player *Player) PlayAt(track string, startTime int, endTime int) error { + start := strconv.Itoa(startTime) + end := strconv.Itoa(endTime) + + msg := message{ + Op: opPlay, + Track: track, + StartTime: start, + EndTime: end, + } + data, err := json.Marshal(msg) + if err != nil { + return err + } + err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) + return err +} + +// Stop will stop the currently playing track +func (player *Player) Stop() error { + msg := message{ + Op: opStop, + GuildID: player.guildID, + } + data, err := json.Marshal(msg) + if err != nil { + return err + } + err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) + return err +} + +// Pause will pause or resume the player, depending on the pause parameter +func (player *Player) Pause(pause bool) error { + msg := message{ + Op: opPause, + GuildID: player.guildID, + Pause: pause, + } + data, err := json.Marshal(msg) + if err != nil { + return err + } + err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) + return err +} + +// Seek will seek the player to the speicifed position, in millis +func (player *Player) Seek(position int) error { + msg := message{ + Op: opSeek, + GuildID: player.guildID, + Position: position, + } + data, err := json.Marshal(msg) + if err != nil { + return err + } + err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) + return err +} + +// Volume will set the player's volume to the specified value +// +// volume must be within [0, 1000] +func (player *Player) Volume(volume int) error { + if volume < 0 || volume > 1000 { + return errVolumeOutOfRange + } + + msg := message{ + Op: opVolume, + GuildID: player.guildID, + Volume: volume, + } + data, err := json.Marshal(msg) + if err != nil { + return err + } + err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) + return err +} + +// Destroy will destroy this player +func (player *Player) Destroy() error { + msg := message{ + Op: opDestroy, + GuildID: player.guildID, + } + data, err := json.Marshal(msg) + if err != nil { + return err + } + err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) + if err != nil { + return err + } + player.manager.players[player.guildID] = nil + return nil +}