Updated README and CHANGELOG
This commit is contained in:
parent
c156d4051f
commit
49940cd08e
|
@ -1,6 +1,9 @@
|
||||||
MumbleDJ Changelog
|
MumbleDJ Changelog
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
### November 15, 2014
|
||||||
|
* Created "v2" branch for Ruby rewrite.
|
||||||
|
|
||||||
### November 9, 2014
|
### November 9, 2014
|
||||||
* Fixed volume changed message showing wrong value.
|
* Fixed volume changed message showing wrong value.
|
||||||
|
|
||||||
|
|
19
README.md
19
README.md
|
@ -1,23 +1,6 @@
|
||||||
MumbleDJ
|
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.
|
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.
|
||||||
|
|
||||||
## 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/)
|
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
[Matthieu Grieger](http://matthieugrieger.com)
|
[Matthieu Grieger](http://matthieugrieger.com)
|
||||||
|
|
|
@ -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 = [[
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><img src="%s" width=150 /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><b><a href="http://youtu.be/%s">%s</a> (%s)</b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center">Added by %s</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
]]
|
|
||||||
|
|
||||||
-- Message shown to channel when a song is added to the queue by a user.
|
|
||||||
config.SONG_ADDED_HTML = [[
|
|
||||||
<b>%s</b> has added "%s" to the queue.
|
|
||||||
]]
|
|
||||||
|
|
||||||
-- Message shown to channel when a user votes to skip a song.
|
|
||||||
config.USER_SKIP_HTML = [[
|
|
||||||
<b>%s</b> 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. <b>Skipping song!</b>
|
|
||||||
]]
|
|
||||||
|
|
||||||
return config
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -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')
|
|
||||||
|
|
|
@ -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
|
|
Reference in a new issue