Reached feature parity with Lua MumbleDJ

This commit is contained in:
Matthieu Grieger 2014-12-27 00:25:49 -08:00
parent 8c15a1d5bd
commit ecc0cd30c1
6 changed files with 182 additions and 109 deletions

View file

@ -8,9 +8,11 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"github.com/kennygrant/sanitize" "github.com/kennygrant/sanitize"
"github.com/layeh/gumble/gumble" "github.com/layeh/gumble/gumble"
"os"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -33,9 +35,16 @@ func parseCommand(user *gumble.User, username, command string) {
if argument == "" { if argument == "" {
user.Send(NO_ARGUMENT_MSG) user.Send(NO_ARGUMENT_MSG)
} else { } else {
success, songTitle := add(username, argument) if songTitle, err := add(username, argument); err == nil {
if success {
dj.client.Self().Channel().Send(fmt.Sprintf(SONG_ADDED_HTML, username, songTitle), false) 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 { } else {
user.Send(INVALID_URL_MSG) user.Send(INVALID_URL_MSG)
} }
@ -45,19 +54,16 @@ func parseCommand(user *gumble.User, username, command string) {
} }
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) {
success := skip(username, false) if err := skip(username, false); err == nil {
if success {
fmt.Println("Skip successful!")
dj.client.Self().Channel().Send(fmt.Sprintf(SKIP_ADDED_HTML, username), false) dj.client.Self().Channel().Send(fmt.Sprintf(SKIP_ADDED_HTML, username), false)
} }
} else { } else {
user.Send(NO_PERMISSION_MSG) user.Send(NO_PERMISSION_MSG)
} }
case dj.conf.Aliases.AdminSkipAlias: case dj.conf.Aliases.AdminSkipAlias:
if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) { if dj.HasPermission(username, true) {
success := skip(username, true) if err := skip(username, true); err == nil {
if success { dj.client.Self().Channel().Send(ADMIN_SONG_SKIP_MSG, false)
fmt.Println("Forceskip successful!")
} }
} else { } else {
user.Send(NO_PERMISSION_MSG) user.Send(NO_PERMISSION_MSG)
@ -67,9 +73,10 @@ func parseCommand(user *gumble.User, username, command string) {
if argument == "" { if argument == "" {
dj.client.Self().Channel().Send(fmt.Sprintf(CUR_VOLUME_HTML, dj.conf.Volume.DefaultVolume), false) dj.client.Self().Channel().Send(fmt.Sprintf(CUR_VOLUME_HTML, dj.conf.Volume.DefaultVolume), false)
} else { } else {
success := volume(username, argument) if err := volume(username, argument); err == nil {
if success { dj.client.Self().Channel().Send(fmt.Sprintf(VOLUME_SUCCESS_HTML, username, argument), false)
fmt.Println("Volume change successful!") } else {
user.Send(NOT_IN_VOLUME_RANGE_MSG)
} }
} }
} else { } else {
@ -80,8 +87,7 @@ func parseCommand(user *gumble.User, username, command string) {
if argument == "" { if argument == "" {
user.Send(NO_ARGUMENT_MSG) user.Send(NO_ARGUMENT_MSG)
} else { } else {
success := move(username, argument) if err := move(argument); err == nil {
if success {
fmt.Printf("%s has been moved to %s.", dj.client.Self().Name(), argument) fmt.Printf("%s has been moved to %s.", dj.client.Self().Name(), argument)
} else { } else {
user.Send(CHANNEL_DOES_NOT_EXIST_MSG) user.Send(CHANNEL_DOES_NOT_EXIST_MSG)
@ -103,9 +109,11 @@ func parseCommand(user *gumble.User, username, command string) {
} }
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) {
success := kill(username) if err := kill(); err == nil {
if success { fmt.Println("Kill successful. Goodbye!")
fmt.Println("Kill successful!") os.Exit(0)
} else {
user.Send(KILL_ERROR_MSG)
} }
} else { } else {
user.Send(NO_PERMISSION_MSG) 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{ youtubePatterns := []string{
`https?:\/\/www\.youtube\.com\/watch\?v=([\w-]+)`, `https?:\/\/www\.youtube\.com\/watch\?v=([\w-]+)`,
`https?:\/\/youtube\.com\/watch\?v=([\w-]+)`, `https?:\/\/youtube\.com\/watch\?v=([\w-]+)`,
@ -126,8 +134,7 @@ func add(user, url string) (bool, string) {
matchFound := false matchFound := false
for _, pattern := range youtubePatterns { for _, pattern := range youtubePatterns {
re, err := regexp.Compile(pattern) if re, err := regexp.Compile(pattern); err == nil {
if err == nil {
if re.MatchString(url) { if re.MatchString(url) {
matchFound = true matchFound = true
break break
@ -139,39 +146,68 @@ func add(user, url string) (bool, string) {
urlMatch := strings.Split(url, "=") urlMatch := strings.Split(url, "=")
shortUrl := urlMatch[1] shortUrl := urlMatch[1]
newSong := NewSong(user, shortUrl) newSong := NewSong(user, shortUrl)
if dj.queue.AddSong(newSong) { if err := dj.queue.AddSong(newSong); err == nil {
return true, newSong.title return newSong.title, nil
} else { } else {
return false, "" return "", errors.New("Could not add the Song to the queue.")
} }
} else { } else {
return false, "" return "", errors.New("The URL provided did not match a YouTube URL.")
} }
} }
func skip(user string, admin bool) bool { func skip(user string, admin bool) error {
return true 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 { func volume(user, value string) error {
parsedVolume, err := strconv.ParseFloat(value, 32) if parsedVolume, err := strconv.ParseFloat(value, 32); err == nil {
if err == nil {
newVolume := float32(parsedVolume) newVolume := float32(parsedVolume)
if newVolume >= dj.conf.Volume.LowestVolume && newVolume <= dj.conf.Volume.HighestVolume { if newVolume >= dj.conf.Volume.LowestVolume && newVolume <= dj.conf.Volume.HighestVolume {
dj.conf.Volume.DefaultVolume = newVolume dj.conf.Volume.DefaultVolume = newVolume
return true return nil
} else { } else {
return false return errors.New("The volume supplied was not in the allowed range.")
} }
} else { } else {
return false return errors.New("An error occurred while parsing the volume string.")
} }
} }
func move(user, channel string) bool { func move(channel string) error {
return true 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 { func kill() error {
return true 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.")
}
} }

37
main.go
View file

@ -11,7 +11,9 @@ import (
"flag" "flag"
"fmt" "fmt"
"github.com/layeh/gumble/gumble" "github.com/layeh/gumble/gumble"
"github.com/layeh/gumble/gumble_ffmpeg"
"github.com/layeh/gumble/gumbleutil" "github.com/layeh/gumble/gumbleutil"
"os/user"
) )
// MumbleDJ type declaration // MumbleDJ type declaration
@ -22,6 +24,9 @@ type mumbledj struct {
defaultChannel string defaultChannel string
conf DjConfig conf DjConfig
queue *SongQueue queue *SongQueue
currentSong *Song
audioStream *gumble_ffmpeg.Stream
homeDir string
} }
func (dj *mumbledj) OnConnect(e *gumble.ConnectEvent) { 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...") fmt.Println("Channel doesn't exist, staying in root channel...")
} }
err := loadConfiguration() if currentUser, err := user.Current(); err == nil {
if err == nil { dj.homeDir = currentUser.HomeDir
}
if err := loadConfiguration(); err == nil {
fmt.Println("Configuration successfully loaded!") fmt.Println("Configuration successfully loaded!")
} else { } else {
panic(err) 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) { 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{ var dj = mumbledj{
keepAlive: make(chan bool), keepAlive: make(chan bool),
queue: NewSongQueue(), queue: NewSongQueue(),

View file

@ -11,7 +11,6 @@ import (
"code.google.com/p/gcfg" "code.google.com/p/gcfg"
"errors" "errors"
"fmt" "fmt"
"os/user"
) )
type DjConfig struct { type DjConfig struct {
@ -46,9 +45,8 @@ type DjConfig struct {
} }
func loadConfiguration() error { func loadConfiguration() error {
usr, err := user.Current() if gcfg.ReadFileInto(&dj.conf, fmt.Sprintf("%s/.mumbledj/config/mumbledj.gcfg", dj.homeDir)) == nil {
if err == nil { return nil
return gcfg.ReadFileInto(&dj.conf, fmt.Sprintf("%s/.mumbledj/config/mumbledj.gcfg", usr.HomeDir))
} else { } else {
return errors.New("Configuration load failed.") return errors.New("Configuration load failed.")
} }

111
song.go
View file

@ -8,15 +8,14 @@
package main package main
import ( import (
//"github.com/layeh/gumble/gumble_ffmpeg"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/jmoiron/jsonq" "github.com/jmoiron/jsonq"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"os/user"
"strings" "strings"
) )
@ -31,12 +30,11 @@ type Song struct {
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)
response, err := http.Get(jsonUrl)
jsonString := "" jsonString := ""
if err == nil {
if response, err := http.Get(jsonUrl); err == nil {
defer response.Body.Close() defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body) if body, err := ioutil.ReadAll(response.Body); err == nil {
if err == nil {
jsonString = string(body) jsonString = string(body)
} }
} }
@ -47,7 +45,7 @@ func NewSong(user, id string) *Song {
jq := jsonq.NewQuery(jsonData) jq := jsonq.NewQuery(jsonData)
videoTitle, _ := jq.String("data", "title") videoTitle, _ := jq.String("data", "title")
videoThumbnail, _ := jq.String("data", "thumbnail", "sqDefault") videoThumbnail, _ := jq.String("data", "thumbnail", "hqDefault")
duration, _ := jq.Int("data", "duration") duration, _ := jq.Int("data", "duration")
videoDuration := fmt.Sprintf("%d:%02d", duration/60, duration%60) videoDuration := fmt.Sprintf("%d:%02d", duration/60, duration%60)
@ -61,58 +59,57 @@ func NewSong(user, id string) *Song {
return song return song
} }
func (s *Song) Download() bool { func (s *Song) Download() error {
err := exec.Command(fmt.Sprintf("youtube-dl --output \"~/.mumbledj/songs/%(id)s.%(ext)s\" --quiet --format m4a %s", s.youtubeId)) cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s.m4a`, s.youtubeId), "--format", "m4a", s.youtubeId)
if err == nil { 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 return true
} else { } else {
return false 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
}

View file

@ -7,6 +7,10 @@
package main package main
import (
"errors"
)
type SongQueue struct { type SongQueue struct {
queue *Queue 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() beforeLen := q.queue.Len()
q.queue.Push(s) q.queue.Push(s)
if q.queue.Len() == beforeLen+1 { if q.queue.Len() == beforeLen+1 {
return true return nil
} else { } else {
return false return errors.New("Could not add Song to the SongQueue.")
} }
return true
} }
func (q *SongQueue) NextSong() *Song { func (q *SongQueue) NextSong() *Song {
return q.queue.Poll().(*Song) return q.queue.Poll().(*Song)
} }
func (q *SongQueue) CurrentSong() *Song { func (q *SongQueue) Len() int {
return q.queue.Peek().(*Song) return q.queue.Len()
} }

View file

@ -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. // 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." 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. // Message shown to user when a successful configuration reload finishes.
const CONFIG_RELOAD_SUCCESS_MSG = "The configuration has been successfully reloaded." 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. // Message shown to a channel when a new song starts playing.
const NOW_PLAYING_HTML = ` const NOW_PLAYING_HTML = `
<table> <table>
@ -69,3 +72,8 @@ const CUR_VOLUME_HTML = `
const SKIP_ADDED_HTML = ` const SKIP_ADDED_HTML = `
<b>%s</b> has voted to skip the current song. <b>%s</b> has voted to skip the current song.
` `
// Message shown to users when they successfully change the volume.
const VOLUME_SUCCESS_HTML = `
<b>%s</b> has changed the volume to <b>%s</b>.
`