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

View File

@ -1,110 +1,110 @@
package gavalink
import (
"errors"
"log"
"os"
"sort"
)
// Log sets the log.Logger gavalink will write to
var Log *log.Logger
func init() {
Log = log.New(os.Stdout, "(gavalink) ", 0)
}
// Lavalink manages a connection to Lavalink Nodes
type Lavalink struct {
shards string
userID string
nodes []Node
players map[string]*Player
}
var (
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")
errNilHandler = errors.New("You must provide an event handler. Use gavalink.DummyEventHandler if you wish to ignore events")
)
// NewLavalink creates a new Lavalink manager
func NewLavalink(shards string, userID string) *Lavalink {
return &Lavalink{
shards: shards,
userID: userID,
/* nodes: make([]Node, 1),*/
players: make(map[string]*Player),
}
}
// AddNodes adds a node to the Lavalink manager
func (lavalink *Lavalink) AddNodes(nodeConfigs ...NodeConfig) error {
nodes := make([]Node, len(nodeConfigs))
for i, c := range nodeConfigs {
n := Node{
config: c,
manager: lavalink,
}
err := n.open()
if err != nil {
return err
}
nodes[i] = n
}
lavalink.nodes = append(lavalink.nodes, nodes...)
return nil
}
// RemoveNode removes a node from the manager
func (lavalink *Lavalink) removeNode(node *Node) error {
idx := -1
for i, n := range lavalink.nodes {
if n == *node {
idx = i
break
}
}
if idx == -1 {
return errNodeNotFound
}
node.stop()
// temp var for easier reading
n := lavalink.nodes
z := len(n) - 1
n[idx] = n[z] // swap idx with last
n = n[:z]
lavalink.nodes = n
return nil
}
// BestNode returns the Node with the lowest latency
func (lavalink *Lavalink) BestNode() (*Node, error) {
if len(lavalink.nodes) < 1 {
return nil, errNoNodes
}
sort.SliceStable(lavalink.nodes, func(i, j int) bool {
return lavalink.nodes[i].load < lavalink.nodes[j].load
})
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
}
package gavalink
import (
"errors"
"log"
"os"
"sort"
)
// Log sets the log.Logger gavalink will write to
var Log *log.Logger
func init() {
Log = log.New(os.Stdout, "(gavalink) ", 0)
}
// Lavalink manages a connection to Lavalink Nodes
type Lavalink struct {
shards string
userID string
nodes []Node
players map[string]*Player
}
var (
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")
errNilHandler = errors.New("You must provide an event handler. Use gavalink.DummyEventHandler if you wish to ignore events")
)
// NewLavalink creates a new Lavalink manager
func NewLavalink(shards string, userID string) *Lavalink {
return &Lavalink{
shards: shards,
userID: userID,
/* nodes: make([]Node, 1),*/
players: make(map[string]*Player),
}
}
// AddNodes adds a node to the Lavalink manager
func (lavalink *Lavalink) AddNodes(nodeConfigs ...NodeConfig) error {
nodes := make([]Node, len(nodeConfigs))
for i, c := range nodeConfigs {
n := Node{
config: c,
manager: lavalink,
}
err := n.open()
if err != nil {
return err
}
nodes[i] = n
}
lavalink.nodes = append(lavalink.nodes, nodes...)
return nil
}
// RemoveNode removes a node from the manager
func (lavalink *Lavalink) removeNode(node *Node) error {
idx := -1
for i, n := range lavalink.nodes {
if n == *node {
idx = i
break
}
}
if idx == -1 {
return errNodeNotFound
}
node.stop()
// temp var for easier reading
n := lavalink.nodes
z := len(n) - 1
n[idx] = n[z] // swap idx with last
n = n[:z]
lavalink.nodes = n
return nil
}
// BestNode returns the Node with the lowest latency
func (lavalink *Lavalink) BestNode() (*Node, error) {
if len(lavalink.nodes) < 1 {
return nil, errNoNodes
}
sort.SliceStable(lavalink.nodes, func(i, j int) bool {
return lavalink.nodes[i].load < lavalink.nodes[j].load
})
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
}

394
node.go
View File

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

326
player.go
View File

@ -1,163 +1,163 @@
package gavalink
import (
"encoding/json"
"strconv"
"github.com/gorilla/websocket"
)
// Player is a Lavalink player
type Player struct {
guildID string
time int
position int
paused bool
manager *Lavalink
node *Node
handler EventHandler
}
// 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,
GuildID: player.guildID,
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 {
player.paused = pause
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
}
// Paused returns whether or not the player is currently paused
func (player *Player) Paused() bool {
return player.paused
}
// 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
}
// Position returns the player's position, as reported by Lavalink
func (player *Player) Position() int {
return player.position
}
// 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
}
// Forward will forward a new VOICE_SERVER_UPDATE to a Lavalink node for
// this player.
//
// This should always be used if a VOICE_SERVER_UPDATE is received for
// a guild which already has a player.
//
// To move a player to a new Node, first player.Destroy() it, and then
// create a new player on the new node.
func (player *Player) Forward(sessionID string, event VoiceServerUpdate) error {
msg := message{
Op: opVoiceUpdate,
GuildID: player.guildID,
SessionID: sessionID,
Event: &event,
}
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
}
package gavalink
import (
"encoding/json"
"strconv"
"github.com/gorilla/websocket"
)
// Player is a Lavalink player
type Player struct {
guildID string
time int
position int
paused bool
manager *Lavalink
node *Node
handler EventHandler
}
// 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,
GuildID: player.guildID,
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 {
player.paused = pause
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
}
// Paused returns whether or not the player is currently paused
func (player *Player) Paused() bool {
return player.paused
}
// 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
}
// Position returns the player's position, as reported by Lavalink
func (player *Player) Position() int {
return player.position
}
// 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
}
// Forward will forward a new VOICE_SERVER_UPDATE to a Lavalink node for
// this player.
//
// This should always be used if a VOICE_SERVER_UPDATE is received for
// a guild which already has a player.
//
// To move a player to a new Node, first player.Destroy() it, and then
// create a new player on the new node.
func (player *Player) Forward(sessionID string, event VoiceServerUpdate) error {
msg := message{
Op: opVoiceUpdate,
GuildID: player.guildID,
SessionID: sessionID,
Event: &event,
}
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
}