Commented code
This commit is contained in:
parent
aa642dcca5
commit
a90b4671e9
|
@ -1,7 +1,7 @@
|
||||||
MumbleDJ Changelog
|
MumbleDJ Changelog
|
||||||
==================
|
==================
|
||||||
|
|
||||||
### December 27, 2014 -- `v2.0.0`
|
### December 27, 2014 -- `v2.0.0, v2.1.0`
|
||||||
* Reached feature parity with old version of MumbleDJ.
|
* Reached feature parity with old version of MumbleDJ.
|
||||||
* Bot is now written completely in Golang instead of Lua and Python.
|
* Bot is now written completely in Golang instead of Lua and Python.
|
||||||
* Now uses [`gumble`](https://github.com/layeh/gumble) for interacting with Mumble instead of [`piepan`](https://github.com/layeh/piepan).
|
* Now uses [`gumble`](https://github.com/layeh/gumble) for interacting with Mumble instead of [`piepan`](https://github.com/layeh/piepan).
|
||||||
|
@ -12,6 +12,7 @@ MumbleDJ Changelog
|
||||||
* Added `mumbledj.gcfg`, where all configuration options are now stored.
|
* Added `mumbledj.gcfg`, where all configuration options are now stored.
|
||||||
* Added a reload command, used to reload the configuration when a change is made.
|
* Added a reload command, used to reload the configuration when a change is made.
|
||||||
* Implemented volume control. Now changes volume while audio is playing!
|
* Implemented volume control. Now changes volume while audio is playing!
|
||||||
|
* Code is now more thoroughly commented.
|
||||||
|
|
||||||
### December 8, 2014
|
### December 8, 2014
|
||||||
* Switched from Ruby to Go, using `gumble` instead of `mumble-ruby` now.
|
* Switched from Ruby to Go, using `gumble` instead of `mumble-ruby` now.
|
||||||
|
|
20
commands.go
20
commands.go
|
@ -18,6 +18,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Called on text message event. Checks the message for a command string, and processes it accordingly if
|
||||||
|
// it contains a command.
|
||||||
func parseCommand(user *gumble.User, username, command string) {
|
func parseCommand(user *gumble.User, username, command string) {
|
||||||
var com, argument string
|
var com, argument string
|
||||||
if strings.Contains(command, " ") {
|
if strings.Contains(command, " ") {
|
||||||
|
@ -30,6 +32,7 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch com {
|
switch com {
|
||||||
|
// Add command
|
||||||
case dj.conf.Aliases.AddAlias:
|
case dj.conf.Aliases.AddAlias:
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminAdd) {
|
if dj.HasPermission(username, dj.conf.Permissions.AdminAdd) {
|
||||||
if argument == "" {
|
if argument == "" {
|
||||||
|
@ -52,6 +55,7 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
} else {
|
} else {
|
||||||
user.Send(NO_PERMISSION_MSG)
|
user.Send(NO_PERMISSION_MSG)
|
||||||
}
|
}
|
||||||
|
// Skip command
|
||||||
case dj.conf.Aliases.SkipAlias:
|
case dj.conf.Aliases.SkipAlias:
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) {
|
if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) {
|
||||||
if err := skip(username, false); err == nil {
|
if err := skip(username, false); err == nil {
|
||||||
|
@ -60,6 +64,7 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
} else {
|
} else {
|
||||||
user.Send(NO_PERMISSION_MSG)
|
user.Send(NO_PERMISSION_MSG)
|
||||||
}
|
}
|
||||||
|
// Forceskip command
|
||||||
case dj.conf.Aliases.AdminSkipAlias:
|
case dj.conf.Aliases.AdminSkipAlias:
|
||||||
if dj.HasPermission(username, true) {
|
if dj.HasPermission(username, true) {
|
||||||
if err := skip(username, true); err == nil {
|
if err := skip(username, true); err == nil {
|
||||||
|
@ -68,6 +73,7 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
} else {
|
} else {
|
||||||
user.Send(NO_PERMISSION_MSG)
|
user.Send(NO_PERMISSION_MSG)
|
||||||
}
|
}
|
||||||
|
// Volume command
|
||||||
case dj.conf.Aliases.VolumeAlias:
|
case dj.conf.Aliases.VolumeAlias:
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminVolume) {
|
if dj.HasPermission(username, dj.conf.Permissions.AdminVolume) {
|
||||||
if argument == "" {
|
if argument == "" {
|
||||||
|
@ -82,6 +88,7 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
} else {
|
} else {
|
||||||
user.Send(NO_PERMISSION_MSG)
|
user.Send(NO_PERMISSION_MSG)
|
||||||
}
|
}
|
||||||
|
// Move command
|
||||||
case dj.conf.Aliases.MoveAlias:
|
case dj.conf.Aliases.MoveAlias:
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminMove) {
|
if dj.HasPermission(username, dj.conf.Permissions.AdminMove) {
|
||||||
if argument == "" {
|
if argument == "" {
|
||||||
|
@ -96,6 +103,7 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
} else {
|
} else {
|
||||||
user.Send(NO_PERMISSION_MSG)
|
user.Send(NO_PERMISSION_MSG)
|
||||||
}
|
}
|
||||||
|
// Reload command
|
||||||
case dj.conf.Aliases.ReloadAlias:
|
case dj.conf.Aliases.ReloadAlias:
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminReload) {
|
if dj.HasPermission(username, dj.conf.Permissions.AdminReload) {
|
||||||
err := loadConfiguration()
|
err := loadConfiguration()
|
||||||
|
@ -107,6 +115,7 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
} else {
|
} else {
|
||||||
user.Send(NO_PERMISSION_MSG)
|
user.Send(NO_PERMISSION_MSG)
|
||||||
}
|
}
|
||||||
|
// Kill command
|
||||||
case dj.conf.Aliases.KillAlias:
|
case dj.conf.Aliases.KillAlias:
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminKill) {
|
if dj.HasPermission(username, dj.conf.Permissions.AdminKill) {
|
||||||
if err := kill(); err == nil {
|
if err := kill(); err == nil {
|
||||||
|
@ -123,6 +132,8 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performs add functionality. Checks input URL for YouTube format, and adds
|
||||||
|
// the URL to the queue if the format matches.
|
||||||
func add(user, url string) (string, error) {
|
func add(user, url string) (string, error) {
|
||||||
youtubePatterns := []string{
|
youtubePatterns := []string{
|
||||||
`https?:\/\/www\.youtube\.com\/watch\?v=([\w-]+)`,
|
`https?:\/\/www\.youtube\.com\/watch\?v=([\w-]+)`,
|
||||||
|
@ -156,6 +167,8 @@ func add(user, url string) (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performs skip functionality. Adds a skip to the skippers slice for the current song, and then
|
||||||
|
// evaluates if a skip should be performed. Both skip and forceskip are implemented here.
|
||||||
func skip(user string, admin bool) error {
|
func skip(user string, admin bool) error {
|
||||||
if err := dj.currentSong.AddSkip(user); err == nil {
|
if err := dj.currentSong.AddSkip(user); err == nil {
|
||||||
if dj.currentSong.SkipReached(len(dj.client.Self().Channel().Users())) || admin {
|
if dj.currentSong.SkipReached(len(dj.client.Self().Channel().Users())) || admin {
|
||||||
|
@ -173,6 +186,9 @@ func skip(user string, admin bool) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performs volume functionality. Checks input value against LowestVolume and HighestVolume from
|
||||||
|
// config to determine if the volume should be applied. If in the correct range, the new volume
|
||||||
|
// is applied and is immediately in effect.
|
||||||
func volume(user, value string) error {
|
func volume(user, value string) error {
|
||||||
if parsedVolume, err := strconv.ParseFloat(value, 32); err == nil {
|
if parsedVolume, err := strconv.ParseFloat(value, 32); err == nil {
|
||||||
newVolume := float32(parsedVolume)
|
newVolume := float32(parsedVolume)
|
||||||
|
@ -187,6 +203,8 @@ func volume(user, value string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performs move functionality. Determines if the supplied channel is valid and moves the bot
|
||||||
|
// to the channel if it is.
|
||||||
func move(channel string) error {
|
func move(channel string) error {
|
||||||
if dj.client.Channels().Find(channel) != nil {
|
if dj.client.Channels().Find(channel) != nil {
|
||||||
dj.client.Self().Move(dj.client.Channels().Find(channel))
|
dj.client.Self().Move(dj.client.Channels().Find(channel))
|
||||||
|
@ -196,6 +214,8 @@ func move(channel string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performs kill functionality. First cleans the ~/.mumbledj/songs directory to get rid of any
|
||||||
|
// excess m4a files. The bot then safely disconnects from the server.
|
||||||
func kill() error {
|
func kill() error {
|
||||||
songsDir := fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir)
|
songsDir := fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir)
|
||||||
if err := os.RemoveAll(songsDir); err != nil {
|
if err := os.RemoveAll(songsDir); err != nil {
|
||||||
|
|
17
main.go
17
main.go
|
@ -29,11 +29,14 @@ type mumbledj struct {
|
||||||
homeDir string
|
homeDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnConnect event. First moves MumbleDJ into the default channel specified
|
||||||
|
// via commandline args, and moves to root channel if the channel does not exist. The current
|
||||||
|
// user's homedir path is stored, configuration is loaded, and the audio stream is set up.
|
||||||
func (dj *mumbledj) OnConnect(e *gumble.ConnectEvent) {
|
func (dj *mumbledj) OnConnect(e *gumble.ConnectEvent) {
|
||||||
if dj.client.Channels().Find(dj.defaultChannel) != nil {
|
if dj.client.Channels().Find(dj.defaultChannel) != nil {
|
||||||
dj.client.Self().Move(dj.client.Channels().Find(dj.defaultChannel))
|
dj.client.Self().Move(dj.client.Channels().Find(dj.defaultChannel))
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Channel doesn't exist, staying in root channel...")
|
fmt.Println("Channel doesn't exist or one was not provided, staying in root channel...")
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentUser, err := user.Current(); err == nil {
|
if currentUser, err := user.Current(); err == nil {
|
||||||
|
@ -55,16 +58,21 @@ func (dj *mumbledj) OnConnect(e *gumble.ConnectEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnDisconnect event. Terminates MumbleDJ thread.
|
||||||
func (dj *mumbledj) OnDisconnect(e *gumble.DisconnectEvent) {
|
func (dj *mumbledj) OnDisconnect(e *gumble.DisconnectEvent) {
|
||||||
dj.keepAlive <- true
|
dj.keepAlive <- true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnTextMessage event. Checks for command prefix, and calls parseCommand if it exists. Ignores
|
||||||
|
// the incoming message otherwise.
|
||||||
func (dj *mumbledj) OnTextMessage(e *gumble.TextMessageEvent) {
|
func (dj *mumbledj) OnTextMessage(e *gumble.TextMessageEvent) {
|
||||||
if e.Message[0] == '!' {
|
if e.Message[0] == dj.conf.General.CommandPrefix {
|
||||||
parseCommand(e.Sender, e.Sender.Name(), e.Message[1:])
|
parseCommand(e.Sender, e.Sender.Name(), e.Message[1:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if username has the permissions to execute a command. Permissions are specified in
|
||||||
|
// mumbledj.gcfg.
|
||||||
func (dj *mumbledj) HasPermission(username string, command bool) bool {
|
func (dj *mumbledj) HasPermission(username string, command bool) bool {
|
||||||
if dj.conf.Permissions.AdminsEnabled && command {
|
if dj.conf.Permissions.AdminsEnabled && command {
|
||||||
for _, adminName := range dj.conf.Permissions.Admins {
|
for _, adminName := range dj.conf.Permissions.Admins {
|
||||||
|
@ -78,6 +86,8 @@ func (dj *mumbledj) HasPermission(username string, command bool) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnSongFinished event. Deletes song that just finished playing, then queues, downloads, and plays
|
||||||
|
// the next song if it exists.
|
||||||
func (dj *mumbledj) OnSongFinished() {
|
func (dj *mumbledj) OnSongFinished() {
|
||||||
if err := dj.currentSong.Delete(); err == nil {
|
if err := dj.currentSong.Delete(); err == nil {
|
||||||
if dj.queue.Len() != 0 {
|
if dj.queue.Len() != 0 {
|
||||||
|
@ -95,11 +105,14 @@ func (dj *mumbledj) OnSongFinished() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dj variable declaration. This is done outside of main() to allow global use.
|
||||||
var dj = mumbledj{
|
var dj = mumbledj{
|
||||||
keepAlive: make(chan bool),
|
keepAlive: make(chan bool),
|
||||||
queue: NewSongQueue(),
|
queue: NewSongQueue(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Main function, but only really performs startup tasks. Grabs and parses commandline
|
||||||
|
// args, sets up the gumble client and its listeners, and then connects to the server.
|
||||||
func main() {
|
func main() {
|
||||||
var address, port, username, password, channel string
|
var address, port, username, password, channel string
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Golang struct representation of mumbledj.gcfg file structure for parsing.
|
||||||
type DjConfig struct {
|
type DjConfig struct {
|
||||||
General struct {
|
General struct {
|
||||||
CommandPrefix string
|
CommandPrefix string
|
||||||
|
@ -44,6 +45,7 @@ type DjConfig struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Loads mumbledj.gcfg into dj.conf, a variable of type DjConfig.
|
||||||
func loadConfiguration() error {
|
func loadConfiguration() error {
|
||||||
if gcfg.ReadFileInto(&dj.conf, fmt.Sprintf("%s/.mumbledj/config/mumbledj.gcfg", dj.homeDir)) == nil {
|
if gcfg.ReadFileInto(&dj.conf, fmt.Sprintf("%s/.mumbledj/config/mumbledj.gcfg", dj.homeDir)) == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
14
song.go
14
song.go
|
@ -19,6 +19,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Song type declaration.
|
||||||
type Song struct {
|
type Song struct {
|
||||||
submitter string
|
submitter string
|
||||||
title string
|
title string
|
||||||
|
@ -28,6 +29,8 @@ type Song struct {
|
||||||
skippers []string
|
skippers []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a new Song type. Before returning the new type, the song's metadata is collected
|
||||||
|
// via the YouTube Gdata API.
|
||||||
func NewSong(user, id string) *Song {
|
func NewSong(user, id string) *Song {
|
||||||
jsonUrl := fmt.Sprintf("http://gdata.youtube.com/feeds/api/videos/%s?v=2&alt=jsonc", id)
|
jsonUrl := fmt.Sprintf("http://gdata.youtube.com/feeds/api/videos/%s?v=2&alt=jsonc", id)
|
||||||
jsonString := ""
|
jsonString := ""
|
||||||
|
@ -59,6 +62,7 @@ func NewSong(user, id string) *Song {
|
||||||
return song
|
return song
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Downloads the song via youtube-dl. All downloaded songs are stored in ~/.mumbledj/songs and should be automatically cleaned.
|
||||||
func (s *Song) Download() error {
|
func (s *Song) Download() error {
|
||||||
cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s.m4a`, s.youtubeId), "--format", "m4a", s.youtubeId)
|
cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s.m4a`, s.youtubeId), "--format", "m4a", s.youtubeId)
|
||||||
if err := cmd.Run(); err == nil {
|
if err := cmd.Run(); err == nil {
|
||||||
|
@ -68,11 +72,14 @@ func (s *Song) Download() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plays the song. Once the song is playing, a notification is displayed in a text message that features the video thumbnail, URL, title,
|
||||||
|
// duration, and submitter.
|
||||||
func (s *Song) Play() {
|
func (s *Song) Play() {
|
||||||
dj.audioStream.Play(fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId))
|
dj.audioStream.Play(fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId))
|
||||||
dj.client.Self().Channel().Send(fmt.Sprintf(NOW_PLAYING_HTML, s.thumbnailUrl, s.youtubeId, s.title, s.duration, s.submitter), false)
|
dj.client.Self().Channel().Send(fmt.Sprintf(NOW_PLAYING_HTML, s.thumbnailUrl, s.youtubeId, s.title, s.duration, s.submitter), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deletes the song from ~/.mumbledj/songs.
|
||||||
func (s *Song) Delete() error {
|
func (s *Song) Delete() error {
|
||||||
filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId)
|
filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId)
|
||||||
if _, err := os.Stat(filePath); err == nil {
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
@ -86,6 +93,8 @@ func (s *Song) Delete() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adds a skip to the skippers slice. If the user is already in the slice AddSkip() returns
|
||||||
|
// an error and does not add a duplicate skip.
|
||||||
func (s *Song) AddSkip(username string) error {
|
func (s *Song) AddSkip(username string) error {
|
||||||
for _, user := range s.skippers {
|
for _, user := range s.skippers {
|
||||||
if username == user {
|
if username == user {
|
||||||
|
@ -96,6 +105,8 @@ func (s *Song) AddSkip(username string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Removes a skip from the skippers slice. If username is not in the slice, an error is
|
||||||
|
// returned.
|
||||||
func (s *Song) RemoveSkip(username string) error {
|
func (s *Song) RemoveSkip(username string) error {
|
||||||
for i, user := range s.skippers {
|
for i, user := range s.skippers {
|
||||||
if username == user {
|
if username == user {
|
||||||
|
@ -106,6 +117,9 @@ func (s *Song) RemoveSkip(username string) error {
|
||||||
return errors.New("This user has not skipped the song.")
|
return errors.New("This user has not skipped the song.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculates current skip ratio based on number of users within MumbleDJ's channel and the
|
||||||
|
// amount of values in the skippers slice. If the value is greater than or equal to the skip ratio
|
||||||
|
// defined in mumbledj.gcfg, the function returns true. Returns false otherwise.
|
||||||
func (s *Song) SkipReached(channelUsers int) bool {
|
func (s *Song) SkipReached(channelUsers int) bool {
|
||||||
if float32(len(s.skippers))/float32(channelUsers) >= dj.conf.General.SkipRatio {
|
if float32(len(s.skippers))/float32(channelUsers) >= dj.conf.General.SkipRatio {
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -11,16 +11,19 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SongQueue type declaration. Serves as a wrapper around the queue structure defined in queue.go.
|
||||||
type SongQueue struct {
|
type SongQueue struct {
|
||||||
queue *Queue
|
queue *Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initializes a new queue and returns the new SongQueue.
|
||||||
func NewSongQueue() *SongQueue {
|
func NewSongQueue() *SongQueue {
|
||||||
return &SongQueue{
|
return &SongQueue{
|
||||||
queue: NewQueue(),
|
queue: NewQueue(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adds a song to the SongQueue.
|
||||||
func (q *SongQueue) AddSong(s *Song) error {
|
func (q *SongQueue) AddSong(s *Song) error {
|
||||||
beforeLen := q.queue.Len()
|
beforeLen := q.queue.Len()
|
||||||
q.queue.Push(s)
|
q.queue.Push(s)
|
||||||
|
@ -31,10 +34,13 @@ func (q *SongQueue) AddSong(s *Song) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Moves to the next song in SongQueue. NextSong() pops the first value of the queue, and is stored
|
||||||
|
// in dj.currentSong.
|
||||||
func (q *SongQueue) NextSong() *Song {
|
func (q *SongQueue) NextSong() *Song {
|
||||||
return q.queue.Poll().(*Song)
|
return q.queue.Poll().(*Song)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the length of the SongQueue.
|
||||||
func (q *SongQueue) Len() int {
|
func (q *SongQueue) Len() int {
|
||||||
return q.queue.Len()
|
return q.queue.Len()
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue