Commented code

pull/18/head 2.1.0
Matthieu Grieger 2014-12-27 10:05:13 -08:00
parent aa642dcca5
commit a90b4671e9
6 changed files with 59 additions and 3 deletions

View File

@ -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.

View File

@ -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 {

17
main.go
View File

@ -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

View File

@ -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

14
song.go
View File

@ -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

View File

@ -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()
}