Reached feature parity with Lua MumbleDJ
This commit is contained in:
parent
8c15a1d5bd
commit
ecc0cd30c1
108
commands.go
108
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.")
|
||||
}
|
||||
}
|
||||
|
|
37
main.go
37
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(),
|
||||
|
|
|
@ -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
111
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
|
||||
}
|
||||
|
|
15
songqueue.go
15
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()
|
||||
}
|
||||
|
|
14
strings.go
14
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 = `
|
||||
<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>.
|
||||
`
|
||||
|
|
Reference in a new issue