diff --git a/CHANGELOG.md b/CHANGELOG.md
index f570490..debef08 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,9 @@
MumbleDJ Changelog
==================
+### November 15, 2014
+* Created "v2" branch for Ruby rewrite.
+
### November 9, 2014
* Fixed volume changed message showing wrong value.
diff --git a/README.md b/README.md
index ba954fc..d055742 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,6 @@
MumbleDJ
========
-A Mumble bot that plays music fetched from YouTube videos. There are ways to play music with a bot on Mumble already, but I wasn't really satisfied with them. Many of them require the Windows client to be opened along with other applications which is not ideal. My goal with this project is to make a Linux-friendly Mumble bot that can run on a webserver instead of a personal computer.
-
-## Setup
-Since the setup process is a bit extensive, the setup guide can be found in [SETUP.md](https://github.com/matthieugrieger/mumbledj/blob/master/SETUP.md).
-
-## Dependencies
-* [OpenSSL](http://www.openssl.org/)
-* [Lua 5.2](http://www.lua.org/)
-* [libev](http://libev.schmorp.de/)
-* [protobuf-c](https://github.com/protobuf-c/protobuf-c)
-* [Ogg Vorbis](https://xiph.org/vorbis/)
-* [Opus](http://www.opus-codec.org/)
-* [Python 2.6 or above](https://www.python.org/)
-* [pafy](https://github.com/np1/pafy/)
-* [piepan](https://github.com/layeh/piepan)
-* [Jansson](http://www.digip.org/jansson/)
-* [jshon](http://kmkeen.com/jshon/)
-* [ffmpeg](https://www.ffmpeg.org/)
+A Mumble bot that plays music fetched from YouTube videos. I have decided to experiment with rewriting the bot in Ruby using [mumble-ruby](https://github.com/perrym5/mumble-ruby). I am hoping this will cut down on the dependency list, make it easier to develop in the future, and allow for some extra functionality that wasn't previously possible.
## Author
[Matthieu Grieger](http://matthieugrieger.com)
diff --git a/mumbledj/config.lua b/mumbledj/config.lua
deleted file mode 100644
index 377ff4e..0000000
--- a/mumbledj/config.lua
+++ /dev/null
@@ -1,174 +0,0 @@
--------------------------
--- MumbleDJ --
--- By Matthieu Grieger --
--------------------------------------------------
--- config.lua --
--- This is where all the configuration options --
--- for the bot can be set. --
--------------------------------------------------
-
-local config = {}
--------------------------
--- GENERAL CONFIGURATION
--------------------------
-
--- Default channel
--- DEFAULT VALUE: "Music"
-config.DEFAULT_CHANNEL = "Music"
-
--- Command prefix
--- DEFAULT VALUE: "!"
-config.COMMAND_PREFIX = "!"
-
--- Show status output in console?
--- DEFAULT VALUE: true
-config.OUTPUT = true
-
--- Default volume (1 being normal volume)
--- DEFAULT VALUE: 0.5
-config.VOLUME = 0.5
-
--- Lowest volume allowed
--- DEFAULT VALUE: 0.01
-config.LOWEST_VOLUME = 0.01
-
--- Highest volume allowed
--- DEFAULT VALUE: 1.5
-config.HIGHEST_VOLUME = 1.5
-
--- Ratio that must be met or exceeded to trigger a song skip.
--- DEFAULT VALUE: 0.5
-config.SKIP_RATIO = 0.5
-
-
--------------------------
--- COMMAND CONFIGURATION
--------------------------
-
--- Alias used for add command.
--- DEFAULT VALUE: "add"
-config.ADD_ALIAS = "add"
-
--- Alias used for skip command.
--- DEFAULT VALUE: "skip"
-config.SKIP_ALIAS = "skip"
-
--- Alias used for volume command.
--- DEFAULT VALUE: "volume"
-config.VOLUME_ALIAS = "volume"
-
--- Alias used for move command.
--- DEFAULT VALUE: "move"
-config.MOVE_ALIAS = "move"
-
--- Alias used for kill command.
--- DEFAULT VALUE: "kill"
-config.KILL_ALIAS = "kill"
-
-
------------------------
--- ADMIN CONFIGURATION
------------------------
-
--- Enable admins (true = on, false = off)
--- DEFAULT VALUE: true
-config.ENABLE_ADMINS = true
-
--- List of admins
--- NOTE: I recommend only giving users admin privileges if they are registered
--- on the server. Otherwise people can just take their username and issue admin
--- commands.
--- EXAMPLE:
--- config.ADMINS = {"Matt", "Matthieu"}
-config.ADMINS = {"Matt", "Matthieu"}
-
--- Make add an admin command?
--- DEFAULT VALUE: false
-config.ADMIN_ADD = false
-
--- Make skip an admin command?
--- DEFAULT VALUE: false
-config.ADMIN_SKIP = false
-
--- Make volume an admin command?
--- DEFAULT VALUE: true
-config.ADMIN_VOLUME = true
-
--- Make move an admin command?
--- DEFAULT VALUE: true
-config.ADMIN_MOVE = true
-
--- Make kill an admin command?
--- DEFAULT VALUE: true (I recommend never changing this to false)
-config.ADMIN_KILL = true
-
-
-----------------------
--- CHAT CONFIGURATION
-----------------------
-
--- Enable/disable chat notifications for new songs (true = on, false = off)
--- DEFAULT VALUE: true
-config.SHOW_NOTIFICATIONS = true
-
--------------------------
--- MESSAGE CONFIGURATION
--------------------------
-
--- Message shown to users when they do not have permission to execute a command.
-config.NO_PERMISSION_MSG = "You do not have permission to execute that command."
-
--- Message shown to users when they try to move the bot to a non-existant channel.
-config.CHANNEL_DOES_NOT_EXIST_MSG = "The channel you specified does not exist."
-
--- Message shown to users when they attempt to add an invalid URL to the queue.
-config.INVALID_URL_MSG = "The URL you submitted does not match the required format. Either you did not provide a YouTube URL, or an error occurred during the downloading & encoding process."
-
--- Message shown to users when they attempt to use the stop command when no music is playing.
-config.NO_MUSIC_PLAYING_MSG = "There is no music playing at the moment."
-
--- Message shown to users when they issue a command that requires an argument and one was not supplied.
-config.NO_ARGUMENT = "The command you issued requires an argument and you did not provide one. Make sure a space exists between the command and the argument."
-
--- Message shown to users when they try to change the volume to a value outside the volume range.
-config.NOT_IN_VOLUME_RANGE = "The volume you tried to supply is not in the allowed volume range. The value must be between " .. config.LOWEST_VOLUME .. " and " .. config.HIGHEST_VOLUME .. "."
-
--- Message shown to users when they successfully change the volume.
-config.VOLUME_SUCCESS = "You have successfully changed the volume to the following: %s."
-
-
-----------------------
--- HTML CONFIGURATION
-----------------------
-
--- Message shown to channel when a new song starts playing.
-config.NOW_PLAYING_HTML = [[
-
-
- ![](%s) |
-
-
- %s (%s) |
-
-
- Added by %s |
-
-
-]]
-
--- Message shown to channel when a song is added to the queue by a user.
-config.SONG_ADDED_HTML = [[
- %s has added "%s" to the queue.
-]]
-
--- Message shown to channel when a user votes to skip a song.
-config.USER_SKIP_HTML = [[
- %s has voted to skip this song.
-]]
-
--- Message shown to channel when a song has been skipped.
-config.SONG_SKIPPED_HTML = [[
- The number of votes required for a skip has been met. Skipping song!
-]]
-
-return config
diff --git a/mumbledj/deque.lua b/mumbledj/deque.lua
deleted file mode 100644
index 7899f91..0000000
--- a/mumbledj/deque.lua
+++ /dev/null
@@ -1,156 +0,0 @@
---Copyright (C) 2013-2014 by Pierre Chapuis
---
---Permission is hereby granted, free of charge, to any person obtaining a copy
---of this software and associated documentation files (the "Software"), to deal
---in the Software without restriction, including without limitation the rights
---to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
---copies of the Software, and to permit persons to whom the Software is
---furnished to do so, subject to the following conditions:
---
---The above copyright notice and this permission notice shall be included in
---all copies or substantial portions of the Software.
---
---THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
---IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
---FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
---AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
---LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
---OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
---THE SOFTWARE.
-
-local push_right = function(self, x)
- assert(x ~= nil)
- self.tail = self.tail + 1
- self[self.tail] = x
-end
-
-local push_left = function(self, x)
- assert(x ~= nil)
- self[self.head] = x
- self.head = self.head - 1
-end
-
-local peek_right = function(self)
- return self[self.tail]
-end
-
-local peek_left = function(self)
- return self[self.head+1]
-end
-
-local pop_right = function(self)
- if self:is_empty() then return nil end
- local r = self[self.tail]
- self[self.tail] = nil
- self.tail = self.tail - 1
- return r
-end
-
-local pop_left = function(self)
- if self:is_empty() then return nil end
- local r = self[self.head+1]
- self.head = self.head + 1
- local r = self[self.head]
- self[self.head] = nil
- return r
-end
-
-local rotate_right = function(self, n)
- n = n or 1
- if self:is_empty() then return nil end
- for i=1,n do self:push_left(self:pop_right()) end
-end
-
-local rotate_left = function(self, n)
- n = n or 1
- if self:is_empty() then return nil end
- for i=1,n do self:push_right(self:pop_left()) end
-end
-
-local _remove_at_internal = function(self, idx)
- for i=idx, self.tail do self[i] = self[i+1] end
- self.tail = self.tail - 1
-end
-
-local remove_right = function(self, x)
- for i=self.tail,self.head+1,-1 do
- if self[i] == x then
- _remove_at_internal(self, i)
- return true
- end
- end
- return false
-end
-
-local remove_left = function(self, x)
- for i=self.head+1,self.tail do
- if self[i] == x then
- _remove_at_internal(self, i)
- return true
- end
- end
- return false
-end
-
-local length = function(self)
- return self.tail - self.head
-end
-
-local is_empty = function(self)
- return self:length() == 0
-end
-
-local contents = function(self)
- local r = {}
- for i=self.head+1,self.tail do
- r[i-self.head] = self[i]
- end
- return r
-end
-
-local iter_right = function(self)
- local i = self.tail+1
- return function()
- if i > self.head+1 then
- i = i-1
- return self[i]
- end
- end
-end
-
-local iter_left = function(self)
- local i = self.head
- return function()
- if i < self.tail then
- i = i+1
- return self[i]
- end
- end
-end
-
-local methods = {
- push_right = push_right,
- push_left = push_left,
- peek_right = peek_right,
- peek_left = peek_left,
- pop_right = pop_right,
- pop_left = pop_left,
- rotate_right = rotate_right,
- rotate_left = rotate_left,
- remove_right = remove_right,
- remove_left = remove_left,
- iter_right = iter_right,
- iter_left = iter_left,
- length = length,
- is_empty = is_empty,
- contents = contents,
-}
-
-local new = function()
- local r = {head = 0, tail = 0}
- return setmetatable(r, {__index = methods})
-end
-
-return {
- new = new,
-}
diff --git a/mumbledj/download_audio.py b/mumbledj/download_audio.py
deleted file mode 100644
index 492a489..0000000
--- a/mumbledj/download_audio.py
+++ /dev/null
@@ -1,44 +0,0 @@
-#---------------------#
-# MumbleDJ #
-# By Matthieu Grieger #
-#---------------------#--------------------------------------------------#
-# download_audio.py #
-# Downloads audio (ogg format) from specified YouTube ID. If no ogg file #
-# exists, it creates an empty file called .video_fail that tells the Lua #
-# side of the program that the download failed. .video_fail will get #
-# deleted on the next successful download. #
-#------------------------------------------------------------------------#
-
-import pafy
-from sys import argv
-from os.path import isfile
-from os import remove, system
-from time import sleep
-
-url = argv[1]
-video = pafy.new(url)
-
-try:
- video.oggstreams[0].download(filepath = 'song.ogg', quiet = True)
- if isfile('.video_fail'):
- remove('.video_fail')
-except:
- with open('.video_fail', 'w+') as f:
- f.close()
-
-while isfile('song.ogg.temp'):
- sleep(1)
-
-if isfile('song.ogg'):
- system('ffmpeg -i song.ogg -acodec libvorbis -ar 48000 -ac 1 -loglevel quiet song-converted.ogg -y')
-else:
- with open('.video_fail', 'w+') as f:
- f.close()
-
-if not isfile('.video_fail'):
- while not isfile('song-converted.ogg'):
- sleep(1)
-
-if isfile('song.ogg'):
- remove('song.ogg')
-
diff --git a/mumbledj/mumbledj.lua b/mumbledj/mumbledj.lua
deleted file mode 100644
index baf2d7b..0000000
--- a/mumbledj/mumbledj.lua
+++ /dev/null
@@ -1,357 +0,0 @@
--------------------------
--- MumbleDJ --
--- By Matthieu Grieger --
-------------------------------------------------------------------
--- mumbledj.lua --
--- The main file which defines most of MumbleDJ's behavior. All --
--- commands are found here, and most of their implementation. --
-------------------------------------------------------------------
-
-local config = require("config")
-local deque = require("deque")
-
--- Connects to Mumble server.
-function piepan.onConnect()
- print(piepan.me.name .. " has connected to the server!")
- local user = piepan.users[piepan.me.name]
- local channel = user.channel(config.DEFAULT_CHANNEL)
- if channel == nil then
- print("The channel '" .. config.DEFAULT_CHANNEL .. "' does not exist. Moving bot to root of server...")
- channel = piepan.channels[0]
- end
- piepan.me:moveTo(channel)
-end
-
--- Function that is called when a new message is posted to the channel.
-function piepan.onMessage(message)
- if message.user == nil then
- return
- end
-
- if string.sub(message.text, 0, 1) == config.COMMAND_PREFIX then
- parse_command(message)
- end
-end
-
--- Parses commands and its arguments (if they exist), and calls the appropriate
--- functions for doing the requested task.
-function parse_command(message)
- local command = ""
- local argument = ""
- if string.find(message.text, " ") then
- command = string.sub(message.text, 2, string.find(message.text, ' ') - 1)
- argument = string.sub(message.text, string.find(message.text, ' ') + 1)
- else
- command = string.sub(message.text, 2)
- end
-
- -- Add command
- if command == config.ADD_ALIAS then
- local has_permission = check_permissions(config.ADMIN_ADD, message.user.name)
-
- if has_permission then
- if config.OUTPUT then
- print(message.user.name .. " has added a song to the queue.")
- if not add_song(argument, message.user.name) then
- message.user:send(config.INVALID_URL_MSG)
- end
- end
- else
- message.user:send(config.NO_PERMISSION_MSG)
- end
- -- Skip command
- elseif command == config.SKIP_ALIAS then
- local has_permission = check_permissions(config.ADMIN_SKIP, message.user.name)
-
- if has_permission then
- if config.OUTPUT then
- print(message.user.name .. " has voted to skip the current song.")
- end
-
- skip(message.user.name)
- else
- message.user:send(config.NO_PERMISSION_MSG)
- end
- -- Volume command
- elseif command == config.VOLUME_ALIAS then
- local has_permission = check_permissions(config.ADMIN_VOLUME, message.user.name)
-
- if has_permission then
- if config.OUTPUT then
- print(message.user.name .. " has changed the volume to the following: " .. argument .. ".")
- if argument ~= nil then
- if config.LOWEST_VOLUME <= tonumber(argument) and tonumber(argument) <= config.HIGHEST_VOLUME then
- config.VOLUME = tonumber(argument)
- message.user:send(string.format(config.VOLUME_SUCCESS, argument))
- else
- message.user:send(config.NOT_IN_VOLUME_RANGE)
- end
- else
- message.user:send(config.NO_ARGUMENT)
- end
- end
- end
- -- Move command
- elseif command == config.MOVE_ALIAS then
- local has_permission = check_permissions(config.ADMIN_MOVE, message.user.name)
-
- if has_permission then
- if config.OUTPUT then
- print(message.user.name .. " has told the bot to move to the following channel: " .. argument .. ".")
- end
- if not move(argument) then
- message.user:send(config.CHANNEL_DOES_NOT_EXIST_MSG)
- end
- else
- message.user:send(config.NO_PERMISSION_MSG)
- end
- -- Kill command
- elseif command == config.KILL_ALIAS then
- local has_permission = check_permissions(config.ADMIN_KILL, message.user.name)
-
- if has_permission then
- if config.OUTPUT then
- print(message.user.name .. " has told the bot to kill itself.")
- end
- kill()
- else
- message.user:send(config.NO_PERMISSION_MSG)
- end
- else
- message.user:send("The command you have entered is not valid.")
- end
-end
-
--- Handles a skip request through the use of helper functions found within
--- song_queue.lua. Once done processing, it will compare the skip ratio with
--- the one defined in the settings and decide whether to skip the current song
--- or not.
-function skip(username)
- if add_skip(username) then
- local skip_ratio = count_skippers() / count_users()
- piepan.me.channel:send(string.format(config.USER_SKIP_HTML, username))
- if skip_ratio > config.SKIP_RATIO then
- piepan.me.channel:send(config.SONG_SKIPPED_HTML)
- piepan.Audio:stop()
- next_song()
- end
- end
-end
-
--- Moves the bot to the channel specified by the "chan" argument.
--- NOTE: This only supports moving to a sibling channel at the moment.
-function move(chan)
- local user = piepan.users[piepan.me.name]
- local channel = user.channel("../" .. chan)
- if channel == nil then
- return false
- else
- piepan.me:moveTo(channel)
- return true
- end
-end
-
--- Performs functions that allow the bot to safely exit.
-function kill()
- os.remove("song.ogg")
- os.remove("song-converted.ogg")
- os.remove(".video_fail")
- piepan.disconnect()
- os.exit(0)
-end
-
--- Checks the permissions of a user against the config to see if they are
--- allowed to execute a certain command.
-function check_permissions(ADMIN_COMMAND, username)
- if config.ENABLE_ADMINS and ADMIN_COMMAND then
- return is_admin(username)
- end
-
- return true
-end
-
--- Checks if a user is an admin, as specified in config.lua.
-function is_admin(username)
- for _,user in pairs(config.ADMINS) do
- if user == username then
- return true
- end
- end
-
- return false
-end
-
--- Switches to the next song.
-function next_song()
- reset_skips()
- if get_length() ~= 0 then
- local success = get_next_song()
- if not success then
- piepan.me.channel:send("An error occurred while preparing the next track. Skipping...")
- end
- end
-end
-
--- Checks if a file exists.
-function file_exists(file)
- local f=io.open(file,"r")
- if f~=nil then io.close(f) return true else return false end
-end
-
--- Returns the number of users in the Mumble server.
-function count_users()
- local user_count = -1 -- Set to -1 to account for the bot
- for name,_ in pairs(piepan.users) do
- user_count = user_count + 1
- end
- return user_count
-end
-
--------------------------------------------------
--- Song Queue Stuff --
--- Contains the definition of the song queue --
--- used for queueing up songs. --
--------------------------------------------------
-
-local song_queue = deque.new()
-local skippers = {}
-
--- Begins the process of adding a new song to the song queue.
-function add_song(url, username)
- local patterns = {
- "https?://www%.youtube%.com/watch%?v=([%d%a_%-]+)",
- "https?://youtube%.com/watch%?v=([%d%a_%-]+)",
- "https?://youtu.be/([%d%a_%-]+)",
- "https?://youtube.com/v/([%d%a_%-]+)",
- "https?://www.youtube.com/v/([%d%a_%-]+)"
- }
-
- for _,pattern in ipairs(patterns) do
- local video_id = string.match(url, pattern)
- if video_id ~= nil and string.len(video_id) < 20 then
- return get_youtube_info(video_id, username)
- else
- return false
- end
- end
-end
-
--- Retrieves the metadata for the specified YouTube video via the gdata API.
-function get_youtube_info(id, username)
- if id == nil then
- return false
- end
- local cmd = [[
- wget -q -O - 'http://gdata.youtube.com/feeds/api/videos/%s?v=2&alt=jsonc' |
- jshon -Q -e data -e title -u -p -e duration -u -p -e thumbnail -e hqDefault -u
- ]]
- local jshon = io.popen(string.format(cmd, id))
- local name = jshon:read()
- local duration = jshon:read()
- local thumbnail = jshon:read()
- if name == nil or duration == nil then
- return false
- end
-
- return youtube_info_completed({
- id = id,
- title = name,
- duration = string.format("%d:%02d", duration / 60, duration % 60),
- thumbnail = thumbnail,
- username = username
- })
-end
-
--- Notifies the channel that a song has been added to the queue, and plays the
--- song if it is the first one in the queue.
-function youtube_info_completed(info)
- if info == nil then
- return false
- end
-
- song_queue:push_right(info)
-
- local message = string.format(config.SONG_ADDED_HTML, info.username, info.title)
- piepan.me.channel:send(message)
-
- if not piepan.Audio.isPlaying() then
- return get_next_song()
- end
-
- return true
-end
-
--- Deletes the old song and begins the process of retrieving a new one.
-function get_next_song()
- reset_skips()
- if file_exists("song-converted.ogg") then
- os.remove("song-converted.ogg")
- end
- if song_queue:length() ~= 0 then
- local next_song = song_queue:pop_left()
- return start_song(next_song)
- end
-end
-
--- Downloads/encodes the audio file and then begins to play it.
-function start_song(info)
- os.execute("python download_audio.py " .. info.id)
- if not file_exists(".video_fail") then
- while not file_exists("song-converted.ogg") do
- os.execute("sleep " .. tonumber(2))
- end
- if piepan.Audio:isPlaying() then
- piepan.Audio:stop()
- end
- piepan.me.channel:play({filename="song-converted.ogg", volume=config.VOLUME}, get_next_song)
- else
- return false
- end
-
- if piepan.Audio:isPlaying() then
- local message = string.format(config.NOW_PLAYING_HTML, info.thumbnail, info.id, info.title, info.duration, info.username)
- piepan.me.channel:send(message)
- return true
- end
-
- return false
-end
-
--- Adds the username of a user who requested a skip. If their name is
--- already in the list nothing will happen.
-function add_skip(username)
- local already_skipped = false
- for _,name in pairs(skippers) do
- if name == username then
- already_skipped = true
- end
- end
- if not already_skipped then
- table.insert(skippers, username)
- return true
- end
-
- return false
-end
-
--- Counts the number of users who would like to skip the current song and
--- returns it.
-function count_skippers()
- local skipper_count = 0
- for name,_ in pairs(skippers) do
- skipper_count = skipper_count + 1
- end
-
- return skipper_count
-end
-
--- Resets the list of users who would like to skip a song. Called during a
--- transition between songs.
-function reset_skips()
- skippers = {}
-end
-
--- Retrieves the length of the song queue and returns it.
-function get_length()
- return song_queue:length()
-end