parent
578ffbd172
commit
3a6aba9545
@ -15,6 +15,7 @@ Bot the can play :
|
|||||||
|
|
||||||
You need to create a folder for all your music. Organize your music by subfolder.
|
You need to create a folder for all your music. Organize your music by subfolder.
|
||||||
The main folder needs to be declared in the config (with a '/' at the end)
|
The main folder needs to be declared in the config (with a '/' at the end)
|
||||||
|
You can enable the web interface into the configuration.ini file.
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
1. You need python 3 with opuslib and protobuf (look at the requirement of pymumble)
|
1. You need python 3 with opuslib and protobuf (look at the requirement of pymumble)
|
||||||
|
@ -24,6 +24,9 @@ announce_current_music = True
|
|||||||
allow_other_channel_message = False
|
allow_other_channel_message = False
|
||||||
allow_private_message = True
|
allow_private_message = True
|
||||||
|
|
||||||
|
# Maximum track played when a playlist is added.
|
||||||
|
max_track_playlist = 20
|
||||||
|
|
||||||
[webinterface]
|
[webinterface]
|
||||||
enabled = False
|
enabled = False
|
||||||
is_web_proxified = True
|
is_web_proxified = True
|
||||||
@ -34,6 +37,7 @@ listening_port = 8181
|
|||||||
play_file = file
|
play_file = file
|
||||||
play_url = url
|
play_url = url
|
||||||
play_radio = radio
|
play_radio = radio
|
||||||
|
play_playlist = playlist
|
||||||
|
|
||||||
help = help
|
help = help
|
||||||
stop = stop
|
stop = stop
|
||||||
@ -74,6 +78,7 @@ pm_not_allowed = Private message aren't allowed.
|
|||||||
help = Command available:
|
help = Command available:
|
||||||
<br />!file [path]
|
<br />!file [path]
|
||||||
<br />!url [url] - youtube or soundcloud
|
<br />!url [url] - youtube or soundcloud
|
||||||
|
<br />!playlist [url] [offset] - youtube or soundcloud playlist (the offset is the track number the bot will start to play - 1 by default)
|
||||||
<br />!radio [url] - url of a stream
|
<br />!radio [url] - url of a stream
|
||||||
<br />!list - display list of available tracks
|
<br />!list - display list of available tracks
|
||||||
<br />!queue - display items in queue
|
<br />!queue - display items in queue
|
||||||
|
2
media.py
2
media.py
@ -108,7 +108,7 @@ def clear_tmp_folder(path, size):
|
|||||||
size_tp += os.path.getsize(file)
|
size_tp += os.path.getsize(file)
|
||||||
if int(size_tp/(1024*1024)) > size:
|
if int(size_tp/(1024*1024)) > size:
|
||||||
logging.info("Cleaning tmp folder")
|
logging.info("Cleaning tmp folder")
|
||||||
to_remove = all_files[idx:]
|
to_remove = all_files[:idx]
|
||||||
print(to_remove)
|
print(to_remove)
|
||||||
for f in to_remove:
|
for f in to_remove:
|
||||||
logging.debug("Removing " + f)
|
logging.debug("Removing " + f)
|
||||||
|
174
mumbleBot.py
174
mumbleBot.py
@ -39,19 +39,21 @@ class MumbleBot:
|
|||||||
######
|
######
|
||||||
## Format of the Playlist :
|
## Format of the Playlist :
|
||||||
## [("<type>","<path/url>")]
|
## [("<type>","<path/url>")]
|
||||||
## types : file, radio, url
|
## types : file, radio, url, is_playlist, number_music_to_play
|
||||||
######
|
######
|
||||||
|
|
||||||
######
|
######
|
||||||
## Format of the current_music variable
|
## Format of the current_music variable
|
||||||
# var.current_music = { "type" : str,
|
# var.current_music = { "type" : str,
|
||||||
# "path" : str,
|
# "path" : str, # path of the file to play
|
||||||
|
# "url" : str # url to download
|
||||||
# "title" : str,
|
# "title" : str,
|
||||||
# "user" : str }
|
# "user" : str,
|
||||||
# len(var.current_music) = 4
|
# "is_playlist": boolean,
|
||||||
# var.current_music["type"] = <Type>
|
# "number_track_to_play": int, # FOR PLAYLIST ONLY
|
||||||
# var.current_music["path"] = <url> if youtube/radio, <path> if file
|
# "start_index" : int, # FOR PLAYLIST ONLY
|
||||||
# var.current_music["title"] = <title>
|
# "current_index" : int} # FOR PLAYLIST ONLY
|
||||||
|
# len(var.current_music) = 6
|
||||||
|
|
||||||
var.playlist = []
|
var.playlist = []
|
||||||
|
|
||||||
@ -161,14 +163,24 @@ class MumbleBot:
|
|||||||
self.mumble.users[text.actor].send_message(msg)
|
self.mumble.users[text.actor].send_message(msg)
|
||||||
else:
|
else:
|
||||||
self.mumble.users[text.actor].send_message(var.config.get('strings', 'bad_file'))
|
self.mumble.users[text.actor].send_message(var.config.get('strings', 'bad_file'))
|
||||||
|
self.async_download_next()
|
||||||
|
|
||||||
elif command == var.config.get('command', 'play_url') and parameter:
|
elif command == var.config.get('command', 'play_url') and parameter:
|
||||||
var.playlist.append(["url", parameter, user])
|
var.playlist.append(["url", parameter, user])
|
||||||
|
self.async_download_next()
|
||||||
|
|
||||||
|
elif command == var.config.get('command', 'play_playlist') and parameter:
|
||||||
|
offset = 1
|
||||||
|
if len(message) > 2:
|
||||||
|
offset = int(message[2])
|
||||||
|
var.playlist.append(["playlist", parameter, user, var.config.getint('bot', 'max_track_playlist'), offset])
|
||||||
|
self.async_download_next()
|
||||||
|
|
||||||
elif command == var.config.get('command', 'play_radio') and parameter:
|
elif command == var.config.get('command', 'play_radio') and parameter:
|
||||||
if var.config.has_option('radio', parameter):
|
if var.config.has_option('radio', parameter):
|
||||||
parameter = var.config.get('radio', parameter)
|
parameter = var.config.get('radio', parameter)
|
||||||
var.playlist.append(["radio", parameter, user])
|
var.playlist.append(["radio", parameter, user])
|
||||||
|
self.async_download_next()
|
||||||
|
|
||||||
elif command == var.config.get('command', 'help'):
|
elif command == var.config.get('command', 'help'):
|
||||||
self.send_msg_channel(var.config.get('strings', 'help'))
|
self.send_msg_channel(var.config.get('strings', 'help'))
|
||||||
@ -186,15 +198,15 @@ class MumbleBot:
|
|||||||
elif command == var.config.get('command', 'update'):
|
elif command == var.config.get('command', 'update'):
|
||||||
if not self.is_admin(user):
|
if not self.is_admin(user):
|
||||||
self.mumble.users[text.actor].send_message("Starting the update")
|
self.mumble.users[text.actor].send_message("Starting the update")
|
||||||
tp = sp.check_output([var.config.get('bot', 'pip3_path'), 'install','--upgrade','youtube-dl']).decode()
|
tp = sp.check_output([var.config.get('bot', 'pip3_path'), 'install', '--upgrade', 'youtube-dl']).decode()
|
||||||
msg=""
|
msg = ""
|
||||||
if "Requirement already up-to-date" in tp:
|
if "Requirement already up-to-date" in tp:
|
||||||
msg += "Youtube-dl is up-to-date"
|
msg += "Youtube-dl is up-to-date"
|
||||||
else :
|
else:
|
||||||
msg += "Update done : " + tp.split('Successfully installed')[1]
|
msg += "Update done : " + tp.split('Successfully installed')[1]
|
||||||
if 'Your branch is up-to-date' in sp.check_output(['/usr/bin/env','git','status']).decode():
|
if 'Your branch is up-to-date' in sp.check_output(['/usr/bin/env', 'git', 'status']).decode():
|
||||||
msg += "<br /> Botamusique is up-to-date"
|
msg += "<br /> Botamusique is up-to-date"
|
||||||
else :
|
else:
|
||||||
msg += "<br /> Botamusique have available update"
|
msg += "<br /> Botamusique have available update"
|
||||||
self.mumble.users[text.actor].send_message(msg)
|
self.mumble.users[text.actor].send_message(msg)
|
||||||
else:
|
else:
|
||||||
@ -210,7 +222,7 @@ class MumbleBot:
|
|||||||
self.volume = float(float(parameter) / 100)
|
self.volume = float(float(parameter) / 100)
|
||||||
self.send_msg_channel(var.config.get('strings', 'change_volume') % (
|
self.send_msg_channel(var.config.get('strings', 'change_volume') % (
|
||||||
int(self.volume * 100), self.mumble.users[text.actor]['name']))
|
int(self.volume * 100), self.mumble.users[text.actor]['name']))
|
||||||
var.config.set('bot', 'volume', str(self.volume))
|
var.db.set('bot', 'volume', str(self.volume))
|
||||||
else:
|
else:
|
||||||
self.send_msg_channel(var.config.get('strings', 'current_volume') % int(self.volume * 100))
|
self.send_msg_channel(var.config.get('strings', 'current_volume') % int(self.volume * 100))
|
||||||
|
|
||||||
@ -233,6 +245,13 @@ class MumbleBot:
|
|||||||
reply = "[file] {title} by {user}".format(
|
reply = "[file] {title} by {user}".format(
|
||||||
title=var.current_music["title"],
|
title=var.current_music["title"],
|
||||||
user=var.current_music["user"])
|
user=var.current_music["user"])
|
||||||
|
elif source == "playlist":
|
||||||
|
reply = "[playlist] {title} (from the playlist <a href=\"{url}\">{playlist}</a> by {user}".format(
|
||||||
|
title=var.current_music["title"],
|
||||||
|
url=var.current_music["path"],
|
||||||
|
playlist=var.current_music["playlist_title"],
|
||||||
|
user=var.current_music["user"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
reply = "(?)[{}] {} {} by {}".format(
|
reply = "(?)[{}] {} {} by {}".format(
|
||||||
var.current_music["type"],
|
var.current_music["type"],
|
||||||
@ -246,13 +265,9 @@ class MumbleBot:
|
|||||||
self.mumble.users[text.actor].send_message(reply)
|
self.mumble.users[text.actor].send_message(reply)
|
||||||
|
|
||||||
elif command == var.config.get('command', 'next'):
|
elif command == var.config.get('command', 'next'):
|
||||||
if var.playlist:
|
if self.get_next():
|
||||||
var.current_music = {'type': var.playlist[0][0],
|
|
||||||
'path': var.playlist[0][1],
|
|
||||||
'title': None,
|
|
||||||
'user': var.playlist[0][2]}
|
|
||||||
var.playlist.pop(0)
|
|
||||||
self.launch_next()
|
self.launch_next()
|
||||||
|
self.async_download_next()
|
||||||
else:
|
else:
|
||||||
self.mumble.users[text.actor].send_message(var.config.get('strings', 'queue_empty'))
|
self.mumble.users[text.actor].send_message(var.config.get('strings', 'queue_empty'))
|
||||||
self.stop()
|
self.stop()
|
||||||
@ -292,25 +307,68 @@ class MumbleBot:
|
|||||||
self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480)
|
self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480)
|
||||||
self.playing = True
|
self.playing = True
|
||||||
|
|
||||||
def is_admin(self, user):
|
@staticmethod
|
||||||
|
def is_admin(user):
|
||||||
list_admin = var.config.get('bot', 'admin').split(';')
|
list_admin = var.config.get('bot', 'admin').split(';')
|
||||||
if user in list_admin:
|
if user in list_admin:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_next():
|
||||||
|
# Return True is next is possible
|
||||||
|
if var.current_music and var.current_music['type'] == "playlist":
|
||||||
|
var.current_music['current_index'] += 1
|
||||||
|
if var.current_music['current_index'] <= (var.current_music['start_index'] + var.current_music['number_track_to_play']):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not var.playlist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if var.playlist[0][0] == "playlist":
|
||||||
|
var.current_music = {'type': var.playlist[0][0],
|
||||||
|
'url': var.playlist[0][1],
|
||||||
|
'title': None,
|
||||||
|
'user': var.playlist[0][2],
|
||||||
|
'is_playlist': True,
|
||||||
|
'number_track_to_play': var.playlist[0][3],
|
||||||
|
'start_index': var.playlist[0][4],
|
||||||
|
'current_index': var.playlist[0][4]
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
var.current_music = {'type': var.playlist[0][0],
|
||||||
|
'url': var.playlist[0][1],
|
||||||
|
'title': None,
|
||||||
|
'user': var.playlist[0][2]}
|
||||||
|
var.playlist.pop(0)
|
||||||
|
return True
|
||||||
|
|
||||||
def launch_next(self):
|
def launch_next(self):
|
||||||
path = ""
|
path = ""
|
||||||
title = ""
|
title = ""
|
||||||
if var.current_music["type"] == "url":
|
var.next_downloaded = False
|
||||||
url = media.get_url(var.current_music["path"])
|
logging.debug(var.current_music)
|
||||||
|
if var.current_music["type"] == "url" or var.current_music["type"] == "playlist":
|
||||||
|
url = media.get_url(var.current_music["url"])
|
||||||
|
|
||||||
if not url:
|
if not url:
|
||||||
return
|
return
|
||||||
media.clear_tmp_folder(var.config.get('bot', 'tmp_folder'), var.config.getint('bot', 'tmp_folder_max_size'))
|
|
||||||
path, title = self.download_music(url)
|
|
||||||
var.current_music["path"] = url
|
|
||||||
|
|
||||||
path_thumbnail = var.config.get('bot', 'tmp_folder') + hashlib.md5(url.encode()).hexdigest() + '.jpg'
|
media.clear_tmp_folder(var.config.get('bot', 'tmp_folder'), var.config.getint('bot', 'tmp_folder_max_size'))
|
||||||
|
|
||||||
|
if var.current_music["type"] == "playlist":
|
||||||
|
path, title = self.download_music(url, var.current_music["current_index"])
|
||||||
|
var.current_music["playlist_title"] = title
|
||||||
|
else:
|
||||||
|
path, title = self.download_music(url)
|
||||||
|
var.current_music["path"] = path
|
||||||
|
|
||||||
|
audio = EasyID3(path)
|
||||||
|
if audio["title"]:
|
||||||
|
title = audio["title"][0]
|
||||||
|
|
||||||
|
path_thumbnail = var.config.get('bot', 'tmp_folder') + hashlib.md5(path.encode()).hexdigest() + '.jpg'
|
||||||
thumbnail_html = ""
|
thumbnail_html = ""
|
||||||
if os.path.isfile(path_thumbnail):
|
if os.path.isfile(path_thumbnail):
|
||||||
im = Image.open(path_thumbnail)
|
im = Image.open(path_thumbnail)
|
||||||
@ -336,6 +394,8 @@ class MumbleBot:
|
|||||||
path = url
|
path = url
|
||||||
title = media.get_radio_server_description(url)
|
title = media.get_radio_server_description(url)
|
||||||
|
|
||||||
|
var.current_music["title"] = title
|
||||||
|
|
||||||
if var.config.getboolean('debug', 'ffmpeg'):
|
if var.config.getboolean('debug', 'ffmpeg'):
|
||||||
ffmpeg_debug = "debug"
|
ffmpeg_debug = "debug"
|
||||||
else:
|
else:
|
||||||
@ -343,15 +403,31 @@ class MumbleBot:
|
|||||||
|
|
||||||
command = ["ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i', path, '-ac', '1', '-f', 's16le', '-ar', '48000', '-']
|
command = ["ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i', path, '-ac', '1', '-f', 's16le', '-ar', '48000', '-']
|
||||||
self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480)
|
self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480)
|
||||||
var.current_music["title"] = title
|
|
||||||
|
|
||||||
def download_music(self, url):
|
@staticmethod
|
||||||
|
def download_music(url, index=None):
|
||||||
url_hash = hashlib.md5(url.encode()).hexdigest()
|
url_hash = hashlib.md5(url.encode()).hexdigest()
|
||||||
|
if index:
|
||||||
|
url_hash = url_hash + "-" + str(index)
|
||||||
path = var.config.get('bot', 'tmp_folder') + url_hash + ".%(ext)s"
|
path = var.config.get('bot', 'tmp_folder') + url_hash + ".%(ext)s"
|
||||||
mp3 = path.replace(".%(ext)s", ".mp3")
|
mp3 = path.replace(".%(ext)s", ".mp3")
|
||||||
if os.path.isfile(mp3):
|
if os.path.isfile(mp3):
|
||||||
audio = EasyID3(mp3)
|
audio = EasyID3(mp3)
|
||||||
video_title = audio["title"][0]
|
video_title = audio["title"][0]
|
||||||
|
else:
|
||||||
|
if index:
|
||||||
|
ydl_opts = {
|
||||||
|
'format': 'bestaudio/best',
|
||||||
|
'outtmpl': path,
|
||||||
|
'writethumbnail': True,
|
||||||
|
'updatetime': False,
|
||||||
|
'playlist_items': str(index),
|
||||||
|
'postprocessors': [{
|
||||||
|
'key': 'FFmpegExtractAudio',
|
||||||
|
'preferredcodec': 'mp3',
|
||||||
|
'preferredquality': '192'},
|
||||||
|
{'key': 'FFmpegMetadata'}]
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
'format': 'bestaudio/best',
|
'format': 'bestaudio/best',
|
||||||
@ -377,6 +453,31 @@ class MumbleBot:
|
|||||||
break
|
break
|
||||||
return mp3, video_title
|
return mp3, video_title
|
||||||
|
|
||||||
|
def async_download_next(self):
|
||||||
|
if not var.next_downloaded:
|
||||||
|
var.next_downloaded = True
|
||||||
|
logging.info("Start download in thread")
|
||||||
|
th = threading.Thread(target=self.download_next, args=())
|
||||||
|
th.daemon = True
|
||||||
|
th.start()
|
||||||
|
|
||||||
|
def download_next(self):
|
||||||
|
if not var.current_music:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if var.current_music["type"] == "playlist":
|
||||||
|
if var.current_music['current_index'] + 1 <= (var.current_music['start_index'] + var.current_music['number_track_to_play']):
|
||||||
|
self.download_music(media.get_url(var.current_music['url']), var.current_music["current_index"] + 1)
|
||||||
|
|
||||||
|
if var.playlist:
|
||||||
|
url = media.get_url(var.playlist[0][1])
|
||||||
|
if not url:
|
||||||
|
return
|
||||||
|
if var.playlist[0][0] == "playlist":
|
||||||
|
self.download_music(url, var.current_music["current_index"])
|
||||||
|
elif var.playlist[0][0] == "playlist":
|
||||||
|
self.download_music(url)
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
raw_music = ""
|
raw_music = ""
|
||||||
while not self.exit and self.mumble.isAlive():
|
while not self.exit and self.mumble.isAlive():
|
||||||
@ -393,10 +494,9 @@ class MumbleBot:
|
|||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
if self.thread is None or not raw_music:
|
if self.thread is None or not raw_music:
|
||||||
if len(var.playlist) != 0:
|
if self.get_next():
|
||||||
var.current_music = {'type': var.playlist[0][0], 'path': var.playlist[0][1], 'user': var.playlist[0][2]}
|
|
||||||
var.playlist.pop(0)
|
|
||||||
self.launch_next()
|
self.launch_next()
|
||||||
|
self.async_download_next()
|
||||||
else:
|
else:
|
||||||
var.current_music = None
|
var.current_music = None
|
||||||
|
|
||||||
@ -405,7 +505,7 @@ class MumbleBot:
|
|||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
if self.exit:
|
if self.exit:
|
||||||
util.write_config()
|
util.write_db()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self.thread:
|
if self.thread:
|
||||||
@ -433,6 +533,8 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
# General arguments
|
# General arguments
|
||||||
parser.add_argument("--config", dest='config', type=str, default='configuration.ini', help='Load configuration from this file. Default: configuration.ini')
|
parser.add_argument("--config", dest='config', type=str, default='configuration.ini', help='Load configuration from this file. Default: configuration.ini')
|
||||||
|
parser.add_argument("--db", dest='db', type=str, default='db.ini', help='database file. Default db.ini')
|
||||||
|
|
||||||
parser.add_argument("-q", "--quiet", dest="quiet", action="store_true", help="Only Error logs")
|
parser.add_argument("-q", "--quiet", dest="quiet", action="store_true", help="Only Error logs")
|
||||||
|
|
||||||
# Mumble arguments
|
# Mumble arguments
|
||||||
@ -443,13 +545,17 @@ if __name__ == '__main__':
|
|||||||
parser.add_argument("-c", "--channel", dest="channel", type=str, help="Default channel for the bot")
|
parser.add_argument("-c", "--channel", dest="channel", type=str, help="Default channel for the bot")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
var.configfile = args.config
|
var.dbfile = args.db
|
||||||
config = configparser.ConfigParser(interpolation=None, allow_no_value=True)
|
config = configparser.ConfigParser(interpolation=None, allow_no_value=True)
|
||||||
parsed_configs = config.read(['configuration.default.ini', var.configfile], encoding='latin-1')
|
parsed_configs = config.read(['configuration.default.ini', args.config, var.dbfile], encoding='latin-1')
|
||||||
|
|
||||||
|
db = configparser.ConfigParser(interpolation=None, allow_no_value=True)
|
||||||
|
db.read([var.dbfile], encoding='latin-1')
|
||||||
|
|
||||||
if len(parsed_configs) == 0:
|
if len(parsed_configs) == 0:
|
||||||
print('Could not read configuration from file \"{}\"'.format(args.config), file=sys.stderr)
|
print('Could not read configuration from file \"{}\"'.format(args.config), file=sys.stderr)
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
var.config = config
|
var.config = config
|
||||||
|
var.db = db
|
||||||
botamusique = MumbleBot(args)
|
botamusique = MumbleBot(args)
|
||||||
|
6
util.py
6
util.py
@ -66,9 +66,9 @@ def zipdir(zippath, zipname_prefix=None):
|
|||||||
return zipname
|
return zipname
|
||||||
|
|
||||||
|
|
||||||
def write_config():
|
def write_db():
|
||||||
with open(var.configfile, 'w') as f:
|
with open(var.dbfile, 'w') as f:
|
||||||
var.config.write(f)
|
var.db.write(f)
|
||||||
|
|
||||||
|
|
||||||
class Dir(object):
|
class Dir(object):
|
||||||
|
@ -3,5 +3,7 @@ playlist = []
|
|||||||
user = ""
|
user = ""
|
||||||
music_folder = ""
|
music_folder = ""
|
||||||
is_proxified = False
|
is_proxified = False
|
||||||
configfile = None
|
dbfile = None
|
||||||
|
db = None
|
||||||
config = None
|
config = None
|
||||||
|
next_downloaded = False
|
||||||
|
Loading…
x
Reference in New Issue
Block a user