From ecc0cd30c1051c95f88be0a987ecd2413f6c5820 Mon Sep 17 00:00:00 2001 From: Matthieu Grieger Date: Sat, 27 Dec 2014 00:25:49 -0800 Subject: [PATCH] Reached feature parity with Lua MumbleDJ --- commands.go | 108 +++++++++++++++++++++++++++++++---------------- main.go | 37 +++++++++++++++-- parseconfig.go | 6 +-- song.go | 111 ++++++++++++++++++++++++------------------------- songqueue.go | 15 ++++--- strings.go | 14 +++++-- 6 files changed, 182 insertions(+), 109 deletions(-) diff --git a/commands.go b/commands.go index 353048d..489394b 100644 --- a/commands.go +++ b/commands.go @@ -8,9 +8,11 @@ package main import ( + "errors" "fmt" "github.com/kennygrant/sanitize" "github.com/layeh/gumble/gumble" + "os" "regexp" "strconv" "strings" @@ -33,9 +35,16 @@ func parseCommand(user *gumble.User, username, command string) { if argument == "" { user.Send(NO_ARGUMENT_MSG) } else { - success, songTitle := add(username, argument) - if success { + if songTitle, err := add(username, argument); err == nil { dj.client.Self().Channel().Send(fmt.Sprintf(SONG_ADDED_HTML, username, songTitle), false) + if dj.queue.Len() == 1 && !dj.audioStream.IsPlaying() { + dj.currentSong = dj.queue.NextSong() + if err := dj.currentSong.Download(); err == nil { + dj.currentSong.Play() + } else { + panic(err) + } + } } else { user.Send(INVALID_URL_MSG) } @@ -45,19 +54,16 @@ func parseCommand(user *gumble.User, username, command string) { } case dj.conf.Aliases.SkipAlias: if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) { - success := skip(username, false) - if success { - fmt.Println("Skip successful!") + if err := skip(username, false); err == nil { dj.client.Self().Channel().Send(fmt.Sprintf(SKIP_ADDED_HTML, username), false) } } else { user.Send(NO_PERMISSION_MSG) } case dj.conf.Aliases.AdminSkipAlias: - if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) { - success := skip(username, true) - if success { - fmt.Println("Forceskip successful!") + if dj.HasPermission(username, true) { + if err := skip(username, true); err == nil { + dj.client.Self().Channel().Send(ADMIN_SONG_SKIP_MSG, false) } } else { user.Send(NO_PERMISSION_MSG) @@ -67,9 +73,10 @@ func parseCommand(user *gumble.User, username, command string) { if argument == "" { dj.client.Self().Channel().Send(fmt.Sprintf(CUR_VOLUME_HTML, dj.conf.Volume.DefaultVolume), false) } else { - success := volume(username, argument) - if success { - fmt.Println("Volume change successful!") + if err := volume(username, argument); err == nil { + dj.client.Self().Channel().Send(fmt.Sprintf(VOLUME_SUCCESS_HTML, username, argument), false) + } else { + user.Send(NOT_IN_VOLUME_RANGE_MSG) } } } else { @@ -80,8 +87,7 @@ func parseCommand(user *gumble.User, username, command string) { if argument == "" { user.Send(NO_ARGUMENT_MSG) } else { - success := move(username, argument) - if success { + if err := move(argument); err == nil { fmt.Printf("%s has been moved to %s.", dj.client.Self().Name(), argument) } else { user.Send(CHANNEL_DOES_NOT_EXIST_MSG) @@ -103,9 +109,11 @@ func parseCommand(user *gumble.User, username, command string) { } case dj.conf.Aliases.KillAlias: if dj.HasPermission(username, dj.conf.Permissions.AdminKill) { - success := kill(username) - if success { - fmt.Println("Kill successful!") + if err := kill(); err == nil { + fmt.Println("Kill successful. Goodbye!") + os.Exit(0) + } else { + user.Send(KILL_ERROR_MSG) } } else { user.Send(NO_PERMISSION_MSG) @@ -115,7 +123,7 @@ func parseCommand(user *gumble.User, username, command string) { } } -func add(user, url string) (bool, string) { +func add(user, url string) (string, error) { youtubePatterns := []string{ `https?:\/\/www\.youtube\.com\/watch\?v=([\w-]+)`, `https?:\/\/youtube\.com\/watch\?v=([\w-]+)`, @@ -126,8 +134,7 @@ func add(user, url string) (bool, string) { matchFound := false for _, pattern := range youtubePatterns { - re, err := regexp.Compile(pattern) - if err == nil { + if re, err := regexp.Compile(pattern); err == nil { if re.MatchString(url) { matchFound = true break @@ -139,39 +146,68 @@ func add(user, url string) (bool, string) { urlMatch := strings.Split(url, "=") shortUrl := urlMatch[1] newSong := NewSong(user, shortUrl) - if dj.queue.AddSong(newSong) { - return true, newSong.title + if err := dj.queue.AddSong(newSong); err == nil { + return newSong.title, nil } else { - return false, "" + return "", errors.New("Could not add the Song to the queue.") } } else { - return false, "" + return "", errors.New("The URL provided did not match a YouTube URL.") } } -func skip(user string, admin bool) bool { - return true +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 { + if err := dj.audioStream.Stop(); err == nil { + dj.OnSongFinished() + return nil + } else { + return errors.New("An error occurred while stopping the current song.") + } + } else { + return errors.New("Not enough skips have been reached to skip the song.") + } + } else { + return errors.New("An error occurred while adding a skip to the current song.") + } } -func volume(user, value string) bool { - parsedVolume, err := strconv.ParseFloat(value, 32) - if err == nil { +func volume(user, value string) error { + if parsedVolume, err := strconv.ParseFloat(value, 32); err == nil { newVolume := float32(parsedVolume) if newVolume >= dj.conf.Volume.LowestVolume && newVolume <= dj.conf.Volume.HighestVolume { dj.conf.Volume.DefaultVolume = newVolume - return true + return nil } else { - return false + return errors.New("The volume supplied was not in the allowed range.") } } else { - return false + return errors.New("An error occurred while parsing the volume string.") } } -func move(user, channel string) bool { - return true +func move(channel string) error { + if dj.client.Channels().Find(channel) != nil { + dj.client.Self().Move(dj.client.Channels().Find(channel)) + return nil + } else { + return errors.New("The channel provided does not exist.") + } } -func kill(user string) bool { - return true +func kill() error { + songsDir := fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir) + if err := os.RemoveAll(songsDir); err != nil { + return errors.New("An error occurred while deleting the audio files.") + } else { + if err := os.Mkdir(songsDir, 0777); err != nil { + return errors.New("An error occurred while recreating the songs directory.") + } + } + if err := dj.client.Disconnect(); err == nil { + return nil + } else { + return errors.New("An error occurred while disconnecting from the server.") + } } diff --git a/main.go b/main.go index 30f9e4d..c2fa5e9 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,9 @@ import ( "flag" "fmt" "github.com/layeh/gumble/gumble" + "github.com/layeh/gumble/gumble_ffmpeg" "github.com/layeh/gumble/gumbleutil" + "os/user" ) // MumbleDJ type declaration @@ -22,6 +24,9 @@ type mumbledj struct { defaultChannel string conf DjConfig queue *SongQueue + currentSong *Song + audioStream *gumble_ffmpeg.Stream + homeDir string } func (dj *mumbledj) OnConnect(e *gumble.ConnectEvent) { @@ -31,13 +36,22 @@ func (dj *mumbledj) OnConnect(e *gumble.ConnectEvent) { fmt.Println("Channel doesn't exist, staying in root channel...") } - err := loadConfiguration() - if err == nil { + if currentUser, err := user.Current(); err == nil { + dj.homeDir = currentUser.HomeDir + } + + if err := loadConfiguration(); err == nil { fmt.Println("Configuration successfully loaded!") } else { panic(err) } - dj.queue = NewSongQueue() + + if audioStream, err := gumble_ffmpeg.New(dj.client); err == nil { + dj.audioStream = audioStream + dj.audioStream.Done = dj.OnSongFinished + } else { + panic(err) + } } func (dj *mumbledj) OnDisconnect(e *gumble.DisconnectEvent) { @@ -63,6 +77,23 @@ func (dj *mumbledj) HasPermission(username string, command bool) bool { } } +func (dj *mumbledj) OnSongFinished() { + if err := dj.currentSong.Delete(); err == nil { + if dj.queue.Len() != 0 { + dj.currentSong = dj.queue.NextSong() + if dj.currentSong != nil { + if err := dj.currentSong.Download(); err == nil { + dj.currentSong.Play() + } else { + panic(err) + } + } + } + } else { + panic(err) + } +} + var dj = mumbledj{ keepAlive: make(chan bool), queue: NewSongQueue(), diff --git a/parseconfig.go b/parseconfig.go index 4ac2e90..8570390 100644 --- a/parseconfig.go +++ b/parseconfig.go @@ -11,7 +11,6 @@ import ( "code.google.com/p/gcfg" "errors" "fmt" - "os/user" ) type DjConfig struct { @@ -46,9 +45,8 @@ type DjConfig struct { } func loadConfiguration() error { - usr, err := user.Current() - if err == nil { - return gcfg.ReadFileInto(&dj.conf, fmt.Sprintf("%s/.mumbledj/config/mumbledj.gcfg", usr.HomeDir)) + if gcfg.ReadFileInto(&dj.conf, fmt.Sprintf("%s/.mumbledj/config/mumbledj.gcfg", dj.homeDir)) == nil { + return nil } else { return errors.New("Configuration load failed.") } diff --git a/song.go b/song.go index 22322ee..70a0b40 100644 --- a/song.go +++ b/song.go @@ -8,15 +8,14 @@ package main import ( - //"github.com/layeh/gumble/gumble_ffmpeg" "encoding/json" + "errors" "fmt" "github.com/jmoiron/jsonq" "io/ioutil" "net/http" "os" "os/exec" - "os/user" "strings" ) @@ -31,12 +30,11 @@ type Song struct { func NewSong(user, id string) *Song { jsonUrl := fmt.Sprintf("http://gdata.youtube.com/feeds/api/videos/%s?v=2&alt=jsonc", id) - response, err := http.Get(jsonUrl) jsonString := "" - if err == nil { + + if response, err := http.Get(jsonUrl); err == nil { defer response.Body.Close() - body, err := ioutil.ReadAll(response.Body) - if err == nil { + if body, err := ioutil.ReadAll(response.Body); err == nil { jsonString = string(body) } } @@ -47,7 +45,7 @@ func NewSong(user, id string) *Song { jq := jsonq.NewQuery(jsonData) videoTitle, _ := jq.String("data", "title") - videoThumbnail, _ := jq.String("data", "thumbnail", "sqDefault") + videoThumbnail, _ := jq.String("data", "thumbnail", "hqDefault") duration, _ := jq.Int("data", "duration") videoDuration := fmt.Sprintf("%d:%02d", duration/60, duration%60) @@ -61,58 +59,57 @@ func NewSong(user, id string) *Song { return song } -func (s *Song) Download() bool { - err := exec.Command(fmt.Sprintf("youtube-dl --output \"~/.mumbledj/songs/%(id)s.%(ext)s\" --quiet --format m4a %s", s.youtubeId)) - if err == nil { +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 { + return nil + } else { + return errors.New("Song download failed.") + } +} + +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) +} + +func (s *Song) Delete() error { + filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId) + if _, err := os.Stat(filePath); err == nil { + if err := os.Remove(filePath); err == nil { + return nil + } else { + return errors.New("Error occurred while deleting audio file.") + } + } else { + return nil + } +} + +func (s *Song) AddSkip(username string) error { + for _, user := range s.skippers { + if username == user { + return errors.New("This user has already skipped the current song.") + } + } + s.skippers = append(s.skippers, username) + return nil +} + +func (s *Song) RemoveSkip(username string) error { + for i, user := range s.skippers { + if username == user { + s.skippers = append(s.skippers[:i], s.skippers[i+1:]...) + return nil + } + } + return errors.New("This user has not skipped the song.") +} + +func (s *Song) SkipReached(channelUsers int) bool { + if float32(len(s.skippers))/float32(channelUsers) >= dj.conf.General.SkipRatio { return true } else { return false } } - -func (s *Song) Play() bool { - return false -} - -func (s *Song) Delete() bool { - usr, err := user.Current() - if err == nil { - filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", usr.HomeDir, s.youtubeId) - if _, err := os.Stat(filePath); err == nil { - err := os.Remove(filePath) - if err == nil { - return true - } else { - return false - } - } else { - return true - } - } else { - return false - } -} - -func (s *Song) AddSkip(username string) bool { - for _, user := range s.skippers { - if username == user { - return false - } - } - s.skippers = append(s.skippers, username) - return true -} - -func (s *Song) RemoveSkip(username string) bool { - for i, user := range s.skippers { - if username == user { - s.skippers = append(s.skippers[:i], s.skippers[i+1:]...) - return true - } - } - return false -} - -func (s *Song) SkipReached(channelUsers int) bool { - return false -} diff --git a/songqueue.go b/songqueue.go index bb3886a..bcf4739 100644 --- a/songqueue.go +++ b/songqueue.go @@ -7,6 +7,10 @@ package main +import ( + "errors" +) + type SongQueue struct { queue *Queue } @@ -17,21 +21,20 @@ func NewSongQueue() *SongQueue { } } -func (q *SongQueue) AddSong(s *Song) bool { +func (q *SongQueue) AddSong(s *Song) error { beforeLen := q.queue.Len() q.queue.Push(s) if q.queue.Len() == beforeLen+1 { - return true + return nil } else { - return false + return errors.New("Could not add Song to the SongQueue.") } - return true } func (q *SongQueue) NextSong() *Song { return q.queue.Poll().(*Song) } -func (q *SongQueue) CurrentSong() *Song { - return q.queue.Peek().(*Song) +func (q *SongQueue) Len() int { + return q.queue.Len() } diff --git a/strings.go b/strings.go index 459816f..e882aab 100644 --- a/strings.go +++ b/strings.go @@ -29,12 +29,15 @@ const NO_ARGUMENT_MSG = "The command you issued requires an argument and you did // Message shown to users when they try to change the volume to a value outside the volume range. const NOT_IN_VOLUME_RANGE_MSG = "Out of range. The volume must be between %g and %g." -// Message shown to users when they successfully change the volume. -const VOLUME_SUCCESS_MSG = "You have successfully changed the volume to the following: %g." - // Message shown to user when a successful configuration reload finishes. const CONFIG_RELOAD_SUCCESS_MSG = "The configuration has been successfully reloaded." +// Message shown to user when an admin skips a song. +const ADMIN_SONG_SKIP_MSG = "An admin has decided to skip the current song." + +// Message shown to user when the kill command errors. +const KILL_ERROR_MSG = "An error occurred while attempting to kill the bot." + // Message shown to a channel when a new song starts playing. const NOW_PLAYING_HTML = ` @@ -69,3 +72,8 @@ const CUR_VOLUME_HTML = ` const SKIP_ADDED_HTML = ` %s has voted to skip the current song. ` + +// Message shown to users when they successfully change the volume. +const VOLUME_SUCCESS_HTML = ` + %s has changed the volume to %s. +`