diff --git a/CHANGELOG.md b/CHANGELOG.md index 5507973..a7d500b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ 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. * 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). @@ -12,6 +12,7 @@ MumbleDJ Changelog * Added `mumbledj.gcfg`, where all configuration options are now stored. * Added a reload command, used to reload the configuration when a change is made. * Implemented volume control. Now changes volume while audio is playing! +* Code is now more thoroughly commented. ### December 8, 2014 * Switched from Ruby to Go, using `gumble` instead of `mumble-ruby` now. diff --git a/commands.go b/commands.go index daf55da..1e35372 100644 --- a/commands.go +++ b/commands.go @@ -18,6 +18,8 @@ import ( "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) { var com, argument string if strings.Contains(command, " ") { @@ -30,6 +32,7 @@ func parseCommand(user *gumble.User, username, command string) { } switch com { + // Add command case dj.conf.Aliases.AddAlias: if dj.HasPermission(username, dj.conf.Permissions.AdminAdd) { if argument == "" { @@ -52,6 +55,7 @@ func parseCommand(user *gumble.User, username, command string) { } else { user.Send(NO_PERMISSION_MSG) } + // Skip command case dj.conf.Aliases.SkipAlias: if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) { if err := skip(username, false); err == nil { @@ -60,6 +64,7 @@ func parseCommand(user *gumble.User, username, command string) { } else { user.Send(NO_PERMISSION_MSG) } + // Forceskip command case dj.conf.Aliases.AdminSkipAlias: if dj.HasPermission(username, true) { if err := skip(username, true); err == nil { @@ -68,6 +73,7 @@ func parseCommand(user *gumble.User, username, command string) { } else { user.Send(NO_PERMISSION_MSG) } + // Volume command case dj.conf.Aliases.VolumeAlias: if dj.HasPermission(username, dj.conf.Permissions.AdminVolume) { if argument == "" { @@ -82,6 +88,7 @@ func parseCommand(user *gumble.User, username, command string) { } else { user.Send(NO_PERMISSION_MSG) } + // Move command case dj.conf.Aliases.MoveAlias: if dj.HasPermission(username, dj.conf.Permissions.AdminMove) { if argument == "" { @@ -96,6 +103,7 @@ func parseCommand(user *gumble.User, username, command string) { } else { user.Send(NO_PERMISSION_MSG) } + // Reload command case dj.conf.Aliases.ReloadAlias: if dj.HasPermission(username, dj.conf.Permissions.AdminReload) { err := loadConfiguration() @@ -107,6 +115,7 @@ func parseCommand(user *gumble.User, username, command string) { } else { user.Send(NO_PERMISSION_MSG) } + // Kill command case dj.conf.Aliases.KillAlias: if dj.HasPermission(username, dj.conf.Permissions.AdminKill) { 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) { youtubePatterns := []string{ `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 { if err := dj.currentSong.AddSkip(user); err == nil { 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 { if parsedVolume, err := strconv.ParseFloat(value, 32); err == nil { 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 { if dj.client.Channels().Find(channel) != nil { 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 { songsDir := fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir) if err := os.RemoveAll(songsDir); err != nil { diff --git a/main.go b/main.go index 552937b..8b98214 100644 --- a/main.go +++ b/main.go @@ -29,11 +29,14 @@ type mumbledj struct { 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) { if dj.client.Channels().Find(dj.defaultChannel) != nil { dj.client.Self().Move(dj.client.Channels().Find(dj.defaultChannel)) } 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 { @@ -55,16 +58,21 @@ func (dj *mumbledj) OnConnect(e *gumble.ConnectEvent) { } } +// OnDisconnect event. Terminates MumbleDJ thread. func (dj *mumbledj) OnDisconnect(e *gumble.DisconnectEvent) { 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) { - if e.Message[0] == '!' { + if e.Message[0] == dj.conf.General.CommandPrefix { 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 { if dj.conf.Permissions.AdminsEnabled && command { 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() { if err := dj.currentSong.Delete(); err == nil { 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{ keepAlive: make(chan bool), 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() { var address, port, username, password, channel string diff --git a/parseconfig.go b/parseconfig.go index 8570390..776e463 100644 --- a/parseconfig.go +++ b/parseconfig.go @@ -13,6 +13,7 @@ import ( "fmt" ) +// Golang struct representation of mumbledj.gcfg file structure for parsing. type DjConfig struct { General struct { CommandPrefix string @@ -44,6 +45,7 @@ type DjConfig struct { } } +// Loads mumbledj.gcfg into dj.conf, a variable of type DjConfig. func loadConfiguration() error { if gcfg.ReadFileInto(&dj.conf, fmt.Sprintf("%s/.mumbledj/config/mumbledj.gcfg", dj.homeDir)) == nil { return nil diff --git a/song.go b/song.go index 70a0b40..5b1db67 100644 --- a/song.go +++ b/song.go @@ -19,6 +19,7 @@ import ( "strings" ) +// Song type declaration. type Song struct { submitter string title string @@ -28,6 +29,8 @@ type Song struct { 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 { jsonUrl := fmt.Sprintf("http://gdata.youtube.com/feeds/api/videos/%s?v=2&alt=jsonc", id) jsonString := "" @@ -59,6 +62,7 @@ func NewSong(user, id string) *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 { cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s.m4a`, s.youtubeId), "--format", "m4a", s.youtubeId) 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() { 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) } +// Deletes the song from ~/.mumbledj/songs. func (s *Song) Delete() error { filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId) 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 { for _, user := range s.skippers { if username == user { @@ -96,6 +105,8 @@ func (s *Song) AddSkip(username string) error { 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 { for i, user := range s.skippers { if username == user { @@ -106,6 +117,9 @@ func (s *Song) RemoveSkip(username string) error { 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 { if float32(len(s.skippers))/float32(channelUsers) >= dj.conf.General.SkipRatio { return true diff --git a/songqueue.go b/songqueue.go index bcf4739..80ea485 100644 --- a/songqueue.go +++ b/songqueue.go @@ -11,16 +11,19 @@ import ( "errors" ) +// SongQueue type declaration. Serves as a wrapper around the queue structure defined in queue.go. type SongQueue struct { queue *Queue } +// Initializes a new queue and returns the new SongQueue. func NewSongQueue() *SongQueue { return &SongQueue{ queue: NewQueue(), } } +// Adds a song to the SongQueue. func (q *SongQueue) AddSong(s *Song) error { beforeLen := q.queue.Len() 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 { return q.queue.Poll().(*Song) } +// Returns the length of the SongQueue. func (q *SongQueue) Len() int { return q.queue.Len() }