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
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.")
}
}

37
main.go
View file

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

View file

@ -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.")
}

111
song.go
View file

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

View file

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

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.
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 = `
<table>
@ -69,3 +72,8 @@ const CUR_VOLUME_HTML = `
const SKIP_ADDED_HTML = `
<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>.
`