feature: add manager, node, track loading
This commit is contained in:
parent
cc6539668e
commit
2e77254b1b
|
@ -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
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue