feature: add manager, node, track loading

This commit is contained in:
Christopher F 2018-08-22 18:00:04 -04:00
parent cc6539668e
commit 2e77254b1b
3 changed files with 258 additions and 0 deletions

76
lavalink.go Normal file
View File

@ -0,0 +1,76 @@
package gavalink
import (
"errors"
)
// Lavalink manages a connection to Lavalink Nodes
type Lavalink struct {
// Shards is the total number of shards the bot is running
Shards int
// UserID is the Discord User ID of the bot
UserID int
nodes []Node
}
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")
)
// NewLavalink creates a new Lavalink manager
func NewLavalink() *Lavalink {
return &Lavalink{}
}
// AddNodes adds a node to the Lavalink manager
func (lavalink *Lavalink) AddNodes(nodeConfigs ...NodeConfig) {
nodes := make([]Node, len(nodeConfigs))
for i, c := range nodeConfigs {
n := Node{
config: c,
shards: lavalink.Shards,
userID: lavalink.UserID,
}
nodes[i] = n
}
lavalink.nodes = append(lavalink.nodes, nodes...)
}
// 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
}
// TODO: lookup latency
return &lavalink.nodes[0], nil
}

53
model.go Normal file
View File

@ -0,0 +1,53 @@
package gavalink
const (
// TrackLoaded is a Tracks Type for a succesful single track load
TrackLoaded = "TRACK_LOADED"
// PlaylistLoaded is a Tracks Type for a succseful playlist load
PlaylistLoaded = "PLAYLIST_LOADED"
// SearchResult is a Tracks Type for a search containing many tracks
SearchResult = "SEARCH_RESULT"
// NoMatches is a Tracks Type for a query yielding no matches
NoMatches = "NO_MATCHES"
// LoadFailed is a Tracks Type for an internal Lavalink error
LoadFailed = "LOAD_FAILED"
)
// Tracks contains data for a Lavalink Tracks response
type Tracks struct {
// Type contains the type of response
//
// This will be one of TrackLoaded, PlaylistLoaded, SearchResult,
// NoMatches, or LoadFailed
Type string `json:"loadType"`
PlaylistInfo *PlaylistInfo `json:"playlistInfo"`
Tracks []Track `json:"tracks"`
}
// PlaylistInfo contains information about a loaded playlist
type PlaylistInfo struct {
// Name is the friendly of the playlist
Name string `json:"name"`
// SelectedTrack is the index of the track that loaded the playlist,
// if one is present.
SelectedTrack int `json:"selectedTrack"`
}
// Track contains information about a loaded track
type Track struct {
// Data contains the base64 encoded Lavaplayer track
Data string `json:"track"`
Info TrackInfo `json:"info"`
}
// TrackInfo contains more data about a loaded track
type TrackInfo struct {
Identifier string `json:"identifier"`
Title string `json:"title"`
Author string `json:"author"`
URI string `json:"uri"`
Seekable bool `json:"isSeekable"`
Stream bool `json:"isStream"`
Length int `json:"length"`
Position int `json:"position"`
}

129
node.go Normal file
View File

@ -0,0 +1,129 @@
package gavalink
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"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
shards int
userID int
manager *Lavalink
wsConn *websocket.Conn
}
func (node *Node) open() error {
header := http.Header{}
header.Set("Authorization", node.config.Password)
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 {
// 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
log.Println(err)
}
}
func (node *Node) onEvent(msgType int, msg []byte) error {
if msgType != websocket.TextMessage {
return errUnknownPayload
}
return 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
}