meta: add editorconfig

This commit is contained in:
Christopher F 2018-08-23 21:24:20 -04:00
parent c472a61322
commit d3b8f06f15
5 changed files with 508 additions and 497 deletions

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
[*.go]
indent_style = tab
indent_size = 4

View File

@ -1,27 +1,27 @@
package gavalink package gavalink
// EventHandler defines events that Lavalink may send to a player // EventHandler defines events that Lavalink may send to a player
type EventHandler interface { type EventHandler interface {
OnTrackEnd(player *Player, track string, reason string) error OnTrackEnd(player *Player, track string, reason string) error
OnTrackException(player *Player, track string, reason string) error OnTrackException(player *Player, track string, reason string) error
OnTrackStuck(player *Player, track string, threshold int) error OnTrackStuck(player *Player, track string, threshold int) error
} }
// DummyEventHandler provides an empty event handler for users who // DummyEventHandler provides an empty event handler for users who
// wish to drop events outright. This is not recommended. // wish to drop events outright. This is not recommended.
type DummyEventHandler struct{} type DummyEventHandler struct{}
// OnTrackEnd is raised when a track ends // OnTrackEnd is raised when a track ends
func (d DummyEventHandler) OnTrackEnd(player *Player, track string, reason string) error { func (d DummyEventHandler) OnTrackEnd(player *Player, track string, reason string) error {
return nil return nil
} }
// OnTrackException is raised when a track throws an exception // OnTrackException is raised when a track throws an exception
func (d DummyEventHandler) OnTrackException(player *Player, track string, reason string) error { func (d DummyEventHandler) OnTrackException(player *Player, track string, reason string) error {
return nil return nil
} }
// OnTrackStuck is raised when a track gets stuck // OnTrackStuck is raised when a track gets stuck
func (d DummyEventHandler) OnTrackStuck(player *Player, track string, threshold int) error { func (d DummyEventHandler) OnTrackStuck(player *Player, track string, threshold int) error {
return nil return nil
} }

View File

@ -1,110 +1,110 @@
package gavalink package gavalink
import ( import (
"errors" "errors"
"log" "log"
"os" "os"
"sort" "sort"
) )
// Log sets the log.Logger gavalink will write to // Log sets the log.Logger gavalink will write to
var Log *log.Logger var Log *log.Logger
func init() { func init() {
Log = log.New(os.Stdout, "(gavalink) ", 0) Log = log.New(os.Stdout, "(gavalink) ", 0)
} }
// Lavalink manages a connection to Lavalink Nodes // Lavalink manages a connection to Lavalink Nodes
type Lavalink struct { type Lavalink struct {
shards string shards string
userID string userID string
nodes []Node nodes []Node
players map[string]*Player players map[string]*Player
} }
var ( var (
errNoNodes = errors.New("No nodes present") errNoNodes = errors.New("No nodes present")
errNodeNotFound = errors.New("Couldn't find that node") errNodeNotFound = errors.New("Couldn't find that node")
errPlayerNotFound = errors.New("Couldn't find a player for that guild") errPlayerNotFound = errors.New("Couldn't find a player for that guild")
errVolumeOutOfRange = errors.New("Volume is out of range, must be within [0, 1000]") errVolumeOutOfRange = errors.New("Volume is out of range, must be within [0, 1000]")
errInvalidVersion = errors.New("This library requires Lavalink >= 3") errInvalidVersion = errors.New("This library requires Lavalink >= 3")
errUnknownPayload = errors.New("Lavalink sent an unknown payload") errUnknownPayload = errors.New("Lavalink sent an unknown payload")
errNilHandler = errors.New("You must provide an event handler. Use gavalink.DummyEventHandler if you wish to ignore events") errNilHandler = errors.New("You must provide an event handler. Use gavalink.DummyEventHandler if you wish to ignore events")
) )
// NewLavalink creates a new Lavalink manager // NewLavalink creates a new Lavalink manager
func NewLavalink(shards string, userID string) *Lavalink { func NewLavalink(shards string, userID string) *Lavalink {
return &Lavalink{ return &Lavalink{
shards: shards, shards: shards,
userID: userID, userID: userID,
/* nodes: make([]Node, 1),*/ /* nodes: make([]Node, 1),*/
players: make(map[string]*Player), players: make(map[string]*Player),
} }
} }
// AddNodes adds a node to the Lavalink manager // AddNodes adds a node to the Lavalink manager
func (lavalink *Lavalink) AddNodes(nodeConfigs ...NodeConfig) error { func (lavalink *Lavalink) AddNodes(nodeConfigs ...NodeConfig) error {
nodes := make([]Node, len(nodeConfigs)) nodes := make([]Node, len(nodeConfigs))
for i, c := range nodeConfigs { for i, c := range nodeConfigs {
n := Node{ n := Node{
config: c, config: c,
manager: lavalink, manager: lavalink,
} }
err := n.open() err := n.open()
if err != nil { if err != nil {
return err return err
} }
nodes[i] = n nodes[i] = n
} }
lavalink.nodes = append(lavalink.nodes, nodes...) lavalink.nodes = append(lavalink.nodes, nodes...)
return nil return nil
} }
// RemoveNode removes a node from the manager // RemoveNode removes a node from the manager
func (lavalink *Lavalink) removeNode(node *Node) error { func (lavalink *Lavalink) removeNode(node *Node) error {
idx := -1 idx := -1
for i, n := range lavalink.nodes { for i, n := range lavalink.nodes {
if n == *node { if n == *node {
idx = i idx = i
break break
} }
} }
if idx == -1 { if idx == -1 {
return errNodeNotFound return errNodeNotFound
} }
node.stop() node.stop()
// temp var for easier reading // temp var for easier reading
n := lavalink.nodes n := lavalink.nodes
z := len(n) - 1 z := len(n) - 1
n[idx] = n[z] // swap idx with last n[idx] = n[z] // swap idx with last
n = n[:z] n = n[:z]
lavalink.nodes = n lavalink.nodes = n
return nil return nil
} }
// BestNode returns the Node with the lowest latency // BestNode returns the Node with the lowest latency
func (lavalink *Lavalink) BestNode() (*Node, error) { func (lavalink *Lavalink) BestNode() (*Node, error) {
if len(lavalink.nodes) < 1 { if len(lavalink.nodes) < 1 {
return nil, errNoNodes return nil, errNoNodes
} }
sort.SliceStable(lavalink.nodes, func(i, j int) bool { sort.SliceStable(lavalink.nodes, func(i, j int) bool {
return lavalink.nodes[i].load < lavalink.nodes[j].load return lavalink.nodes[i].load < lavalink.nodes[j].load
}) })
return &lavalink.nodes[0], nil return &lavalink.nodes[0], nil
} }
// GetPlayer gets a player for a guild // GetPlayer gets a player for a guild
func (lavalink *Lavalink) GetPlayer(guild string) (*Player, error) { func (lavalink *Lavalink) GetPlayer(guild string) (*Player, error) {
p, ok := lavalink.players[guild] p, ok := lavalink.players[guild]
if !ok { if !ok {
return nil, errPlayerNotFound return nil, errPlayerNotFound
} }
return p, nil return p, nil
} }

394
node.go
View File

@ -1,197 +1,197 @@
package gavalink package gavalink
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
// NodeConfig configures a Lavalink Node // NodeConfig configures a Lavalink Node
type NodeConfig struct { type NodeConfig struct {
// REST is the host where Lavalink's REST server runs // REST is the host where Lavalink's REST server runs
// //
// This value is expected without a trailing slash, e.g. like // This value is expected without a trailing slash, e.g. like
// `http://localhost:2333` // `http://localhost:2333`
REST string REST string
// WebSocket is the host where Lavalink's WebSocket server runs // WebSocket is the host where Lavalink's WebSocket server runs
// //
// This value is expected without a trailing slash, e.g. like // This value is expected without a trailing slash, e.g. like
// `http://localhost:8012` // `http://localhost:8012`
WebSocket string WebSocket string
// Password is the expected Authorization header for the Node // Password is the expected Authorization header for the Node
Password string Password string
} }
// Node wraps a Lavalink Node // Node wraps a Lavalink Node
type Node struct { type Node struct {
config NodeConfig config NodeConfig
load float32 load float32
manager *Lavalink manager *Lavalink
wsConn *websocket.Conn wsConn *websocket.Conn
} }
func (node *Node) open() error { func (node *Node) open() error {
header := http.Header{} header := http.Header{}
header.Set("Authorization", node.config.Password) header.Set("Authorization", node.config.Password)
header.Set("Num-Shards", node.manager.shards) header.Set("Num-Shards", node.manager.shards)
header.Set("User-Id", node.manager.userID) header.Set("User-Id", node.manager.userID)
ws, resp, err := websocket.DefaultDialer.Dial(node.config.WebSocket, header) ws, resp, err := websocket.DefaultDialer.Dial(node.config.WebSocket, header)
if err != nil { if err != nil {
return err return err
} }
vstr := resp.Header.Get("Lavalink-Major-Version") vstr := resp.Header.Get("Lavalink-Major-Version")
v, err := strconv.Atoi(vstr) v, err := strconv.Atoi(vstr)
if err != nil { if err != nil {
return err return err
} }
if v < 3 { if v < 3 {
return errInvalidVersion return errInvalidVersion
} }
node.wsConn = ws node.wsConn = ws
go node.listen() go node.listen()
Log.Println("node", node.config.WebSocket, "opened") Log.Println("node", node.config.WebSocket, "opened")
return nil return nil
} }
func (node *Node) stop() { func (node *Node) stop() {
// someone already stopped this // someone already stopped this
if node.wsConn == nil { if node.wsConn == nil {
return return
} }
_ = node.wsConn.Close() _ = node.wsConn.Close()
} }
func (node *Node) listen() { func (node *Node) listen() {
for { for {
msgType, msg, err := node.wsConn.ReadMessage() msgType, msg, err := node.wsConn.ReadMessage()
if err != nil { if err != nil {
Log.Println(err) Log.Println(err)
// try to reconnect // try to reconnect
oerr := node.open() oerr := node.open()
if oerr != nil { if oerr != nil {
Log.Println("node", node.config.WebSocket, "failed and could not reconnect, destroying.", err, oerr) Log.Println("node", node.config.WebSocket, "failed and could not reconnect, destroying.", err, oerr)
node.manager.removeNode(node) node.manager.removeNode(node)
return return
} }
Log.Println("node", node.config.WebSocket, "reconnected") Log.Println("node", node.config.WebSocket, "reconnected")
return return
} }
err = node.onEvent(msgType, msg) err = node.onEvent(msgType, msg)
// TODO: better error handling? // TODO: better error handling?
if err != nil { if err != nil {
Log.Println(err) Log.Println(err)
} }
} }
} }
func (node *Node) onEvent(msgType int, msg []byte) error { func (node *Node) onEvent(msgType int, msg []byte) error {
if msgType != websocket.TextMessage { if msgType != websocket.TextMessage {
return errUnknownPayload return errUnknownPayload
} }
m := message{} m := message{}
err := json.Unmarshal(msg, &m) err := json.Unmarshal(msg, &m)
if err != nil { if err != nil {
return err return err
} }
switch m.Op { switch m.Op {
case opPlayerUpdate: case opPlayerUpdate:
player, err := node.manager.GetPlayer(m.GuildID) player, err := node.manager.GetPlayer(m.GuildID)
if err != nil { if err != nil {
return err return err
} }
player.time = m.State.Time player.time = m.State.Time
player.position = m.State.Position player.position = m.State.Position
case opEvent: case opEvent:
player, err := node.manager.GetPlayer(m.GuildID) player, err := node.manager.GetPlayer(m.GuildID)
if err != nil { if err != nil {
return err return err
} }
switch m.Type { switch m.Type {
case eventTrackEnd: case eventTrackEnd:
err = player.handler.OnTrackEnd(player, m.Track, m.Reason) err = player.handler.OnTrackEnd(player, m.Track, m.Reason)
case eventTrackException: case eventTrackException:
err = player.handler.OnTrackException(player, m.Track, m.Reason) err = player.handler.OnTrackException(player, m.Track, m.Reason)
case eventTrackStuck: case eventTrackStuck:
err = player.handler.OnTrackStuck(player, m.Track, m.ThresholdMs) err = player.handler.OnTrackStuck(player, m.Track, m.ThresholdMs)
} }
return err return err
case opStats: case opStats:
node.load = m.StatCPU.Load node.load = m.StatCPU.Load
Log.Println("dbg-node", node.config.WebSocket, "load", node.load) Log.Println("dbg-node", node.config.WebSocket, "load", node.load)
default: default:
return errUnknownPayload return errUnknownPayload
} }
return nil return nil
} }
// CreatePlayer creates an audio player on this node // CreatePlayer creates an audio player on this node
func (node *Node) CreatePlayer(guildID string, sessionID string, event VoiceServerUpdate, handler EventHandler) (*Player, error) { func (node *Node) CreatePlayer(guildID string, sessionID string, event VoiceServerUpdate, handler EventHandler) (*Player, error) {
msg := message{ msg := message{
Op: opVoiceUpdate, Op: opVoiceUpdate,
GuildID: guildID, GuildID: guildID,
SessionID: sessionID, SessionID: sessionID,
Event: &event, Event: &event,
} }
data, err := json.Marshal(msg) data, err := json.Marshal(msg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = node.wsConn.WriteMessage(websocket.TextMessage, data) err = node.wsConn.WriteMessage(websocket.TextMessage, data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
player := &Player{ player := &Player{
guildID: guildID, guildID: guildID,
manager: node.manager, manager: node.manager,
node: node, node: node,
handler: handler, handler: handler,
} }
node.manager.players[guildID] = player node.manager.players[guildID] = player
return player, nil return player, nil
} }
// LoadTracks queries lavalink to return a Tracks object // LoadTracks queries lavalink to return a Tracks object
// //
// query should be a valid Lavaplayer query, including but not limited to: // query should be a valid Lavaplayer query, including but not limited to:
// - A direct media URI // - A direct media URI
// - A direct Youtube /watch URI // - A direct Youtube /watch URI
// - A search query, prefixed with ytsearch: or scsearch: // - A search query, prefixed with ytsearch: or scsearch:
// //
// See the Lavaplayer Source Code for all valid options. // See the Lavaplayer Source Code for all valid options.
func (node *Node) LoadTracks(query string) (*Tracks, error) { func (node *Node) LoadTracks(query string) (*Tracks, error) {
url := fmt.Sprintf("%s/loadtracks?identifier=%s", node.config.REST, query) url := fmt.Sprintf("%s/loadtracks?identifier=%s", node.config.REST, query)
req, err := http.NewRequest(http.MethodGet, url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header.Set("Authorization", node.config.Password) req.Header.Set("Authorization", node.config.Password)
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
data, err := ioutil.ReadAll(resp.Body) data, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tracks := new(Tracks) tracks := new(Tracks)
err = json.Unmarshal(data, &tracks) err = json.Unmarshal(data, &tracks)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return tracks, nil return tracks, nil
} }

326
player.go
View File

@ -1,163 +1,163 @@
package gavalink package gavalink
import ( import (
"encoding/json" "encoding/json"
"strconv" "strconv"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
// Player is a Lavalink player // Player is a Lavalink player
type Player struct { type Player struct {
guildID string guildID string
time int time int
position int position int
paused bool paused bool
manager *Lavalink manager *Lavalink
node *Node node *Node
handler EventHandler handler EventHandler
} }
// Play will play the given track completely // Play will play the given track completely
func (player *Player) Play(track string) error { func (player *Player) Play(track string) error {
return player.PlayAt(track, 0, 0) return player.PlayAt(track, 0, 0)
} }
// PlayAt will play the given track at the specified start and end times // PlayAt will play the given track at the specified start and end times
// //
// Setting a time to 0 will omit it. // Setting a time to 0 will omit it.
func (player *Player) PlayAt(track string, startTime int, endTime int) error { func (player *Player) PlayAt(track string, startTime int, endTime int) error {
start := strconv.Itoa(startTime) start := strconv.Itoa(startTime)
end := strconv.Itoa(endTime) end := strconv.Itoa(endTime)
msg := message{ msg := message{
Op: opPlay, Op: opPlay,
GuildID: player.guildID, GuildID: player.guildID,
Track: track, Track: track,
StartTime: start, StartTime: start,
EndTime: end, EndTime: end,
} }
data, err := json.Marshal(msg) data, err := json.Marshal(msg)
if err != nil { if err != nil {
return err return err
} }
err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) err = player.node.wsConn.WriteMessage(websocket.TextMessage, data)
return err return err
} }
// Stop will stop the currently playing track // Stop will stop the currently playing track
func (player *Player) Stop() error { func (player *Player) Stop() error {
msg := message{ msg := message{
Op: opStop, Op: opStop,
GuildID: player.guildID, GuildID: player.guildID,
} }
data, err := json.Marshal(msg) data, err := json.Marshal(msg)
if err != nil { if err != nil {
return err return err
} }
err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) err = player.node.wsConn.WriteMessage(websocket.TextMessage, data)
return err return err
} }
// Pause will pause or resume the player, depending on the pause parameter // Pause will pause or resume the player, depending on the pause parameter
func (player *Player) Pause(pause bool) error { func (player *Player) Pause(pause bool) error {
player.paused = pause player.paused = pause
msg := message{ msg := message{
Op: opPause, Op: opPause,
GuildID: player.guildID, GuildID: player.guildID,
Pause: &pause, Pause: &pause,
} }
data, err := json.Marshal(msg) data, err := json.Marshal(msg)
if err != nil { if err != nil {
return err return err
} }
err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) err = player.node.wsConn.WriteMessage(websocket.TextMessage, data)
return err return err
} }
// Paused returns whether or not the player is currently paused // Paused returns whether or not the player is currently paused
func (player *Player) Paused() bool { func (player *Player) Paused() bool {
return player.paused return player.paused
} }
// Seek will seek the player to the speicifed position, in millis // Seek will seek the player to the speicifed position, in millis
func (player *Player) Seek(position int) error { func (player *Player) Seek(position int) error {
msg := message{ msg := message{
Op: opSeek, Op: opSeek,
GuildID: player.guildID, GuildID: player.guildID,
Position: &position, Position: &position,
} }
data, err := json.Marshal(msg) data, err := json.Marshal(msg)
if err != nil { if err != nil {
return err return err
} }
err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) err = player.node.wsConn.WriteMessage(websocket.TextMessage, data)
return err return err
} }
// Position returns the player's position, as reported by Lavalink // Position returns the player's position, as reported by Lavalink
func (player *Player) Position() int { func (player *Player) Position() int {
return player.position return player.position
} }
// Volume will set the player's volume to the specified value // Volume will set the player's volume to the specified value
// //
// volume must be within [0, 1000] // volume must be within [0, 1000]
func (player *Player) Volume(volume int) error { func (player *Player) Volume(volume int) error {
if volume < 0 || volume > 1000 { if volume < 0 || volume > 1000 {
return errVolumeOutOfRange return errVolumeOutOfRange
} }
msg := message{ msg := message{
Op: opVolume, Op: opVolume,
GuildID: player.guildID, GuildID: player.guildID,
Volume: &volume, Volume: &volume,
} }
data, err := json.Marshal(msg) data, err := json.Marshal(msg)
if err != nil { if err != nil {
return err return err
} }
err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) err = player.node.wsConn.WriteMessage(websocket.TextMessage, data)
return err return err
} }
// Forward will forward a new VOICE_SERVER_UPDATE to a Lavalink node for // Forward will forward a new VOICE_SERVER_UPDATE to a Lavalink node for
// this player. // this player.
// //
// This should always be used if a VOICE_SERVER_UPDATE is received for // This should always be used if a VOICE_SERVER_UPDATE is received for
// a guild which already has a player. // a guild which already has a player.
// //
// To move a player to a new Node, first player.Destroy() it, and then // To move a player to a new Node, first player.Destroy() it, and then
// create a new player on the new node. // create a new player on the new node.
func (player *Player) Forward(sessionID string, event VoiceServerUpdate) error { func (player *Player) Forward(sessionID string, event VoiceServerUpdate) error {
msg := message{ msg := message{
Op: opVoiceUpdate, Op: opVoiceUpdate,
GuildID: player.guildID, GuildID: player.guildID,
SessionID: sessionID, SessionID: sessionID,
Event: &event, Event: &event,
} }
data, err := json.Marshal(msg) data, err := json.Marshal(msg)
if err != nil { if err != nil {
return err return err
} }
err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) err = player.node.wsConn.WriteMessage(websocket.TextMessage, data)
return err return err
} }
// Destroy will destroy this player // Destroy will destroy this player
func (player *Player) Destroy() error { func (player *Player) Destroy() error {
msg := message{ msg := message{
Op: opDestroy, Op: opDestroy,
GuildID: player.guildID, GuildID: player.guildID,
} }
data, err := json.Marshal(msg) data, err := json.Marshal(msg)
if err != nil { if err != nil {
return err return err
} }
err = player.node.wsConn.WriteMessage(websocket.TextMessage, data) err = player.node.wsConn.WriteMessage(websocket.TextMessage, data)
if err != nil { if err != nil {
return err return err
} }
player.manager.players[player.guildID] = nil player.manager.players[player.guildID] = nil
return nil return nil
} }