Add more controls (#71)

* add more ducking command

* fix current music command

* provide more controls, like pause, resume, clear.

* add more controls in the web interface

* refactored and improved:
1. move get_music_tag_info to util, and
2. refined logic related to it.
3. now playlist will check for tag info thus update_music_tag_info is
useless and was removed
4. add "add folder" feature to !file asked in #65,
5. fix bugs related to !file

* truncate file list if too long

* fixed several tiny bugs

* fixed several tiny bugs continue

* fixed several tiny bugs continue continue

* fixed several tiny bugs continue**3

* fixed several tiny bugs continue**4

* added !filematch command to add files that match a regex pattern.

* truncate long message

* fix web interface delete file issue

* refresh after delete file

* add regex to listfile command

* refactored command part, added partial match support for commands

* organized

* added random command

* typo

* typo

* Fixed many bugs.

* Added workaround for azlux/pymumble#44 to fix the memory leak.

* changed logging style.

* fixed bugs related to random and resume

* fix now playing

* fixed issue related to download

* fixed issue related to download 2

* fixed thumbnail issue

* fixed add url problem in web interface

* REFACTORED, turned db.ini into sqlite3 database.

* fixed remove song problem

* fixed radio get title problem. auto download if tmp file is deleted

* fixed current index not loaded from database

* fixed: order of songs loaded from the database

* fixed: some obscure bugs. beautified error of commands

* added a workaround for TerryGeng/botamusique#1.

* beautified

* fixed: channel not loaded in the config

* fixed: auto checked for updates

* fixed: mysterious bug: sometimes "now playing" string cannot be properly
displayed.
The real reason is: do use <br />, do not use <br>. I tried hours to
find out this.

* chore: unified debug messages that refer to music items

* feav: fetch ffmpeg stderr mentioned in #72, reformatted logs.

* fix: async download not working

* fix: async download not working, still

* fix: async download not working, finished

* feat: queue command: ▶current playing item◀

* feat: support more than one command prefix

* chore: added some WARNINGs into default config file to avoid people to touch it.

* refactor: packed all string contants into constants.py, just to avoid people messing it around.

* refactor: required by azlux. Added a configuration.example.ini to keep people away from configuration.default.ini
This commit is contained in:
Terry Geng 2020-02-25 02:17:02 +08:00 committed by GitHub
parent 951934602e
commit 97f2326d9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1684 additions and 835 deletions

4
.gitignore vendored
View File

@ -110,4 +110,6 @@ configuration.ini
2019-07-27 22_09_08-radiobrowser.py - botamusique - Visual Studio Code.png
music_folder/
tmp/
tmp/
database.db

602
command.py Normal file
View File

@ -0,0 +1,602 @@
# coding=utf-8
import logging
import os.path
import pymumble.pymumble_py3 as pymumble
import re
import constants
import media.file
import media.playlist
import media.radio
import media.system
import media.url
import util
import variables as var
from librb import radiobrowser
from database import Database
def register_all_commands(bot):
bot.register_command(constants.commands('joinme'), cmd_joinme)
bot.register_command(constants.commands('user_ban'), cmd_user_ban)
bot.register_command(constants.commands('user_unban'), cmd_user_unban)
bot.register_command(constants.commands('url_ban'), cmd_url_ban)
bot.register_command(constants.commands('url_unban'), cmd_url_unban)
bot.register_command(constants.commands('play'), cmd_play)
bot.register_command(constants.commands('pause'), cmd_pause)
bot.register_command(constants.commands('play_file'), cmd_play_file)
bot.register_command(constants.commands('play_file_match'), cmd_play_file_match)
bot.register_command(constants.commands('play_url'), cmd_play_url)
bot.register_command(constants.commands('play_playlist'), cmd_play_playlist)
bot.register_command(constants.commands('play_radio'), cmd_play_radio)
bot.register_command(constants.commands('rb_query'), cmd_rb_query)
bot.register_command(constants.commands('rb_play'), cmd_rb_play)
bot.register_command(constants.commands('help'), cmd_help)
bot.register_command(constants.commands('stop'), cmd_stop)
bot.register_command(constants.commands('clear'), cmd_clear)
bot.register_command(constants.commands('kill'), cmd_kill)
bot.register_command(constants.commands('update'), cmd_update)
bot.register_command(constants.commands('stop_and_getout'), cmd_stop_and_getout)
bot.register_command(constants.commands('volume'), cmd_volume)
bot.register_command(constants.commands('ducking'), cmd_ducking)
bot.register_command(constants.commands('ducking_threshold'), cmd_ducking_threshold)
bot.register_command(constants.commands('ducking_volume'), cmd_ducking_volume)
bot.register_command(constants.commands('current_music'), cmd_current_music)
bot.register_command(constants.commands('skip'), cmd_skip)
bot.register_command(constants.commands('remove'), cmd_remove)
bot.register_command(constants.commands('list_file'), cmd_list_file)
bot.register_command(constants.commands('queue'), cmd_queue)
bot.register_command(constants.commands('random'), cmd_random)
bot.register_command(constants.commands('drop_database'), cmd_drop_database)
def send_multi_lines(bot, lines, text):
msg = ""
br = ""
for newline in lines:
msg += br
br = "<br>"
if len(msg) + len(newline) > 5000:
bot.send_msg(msg, text)
msg = ""
msg += newline
bot.send_msg(msg, text)
# ---------------- Commands ------------------
def cmd_joinme(bot, user, text, command, parameter):
channel_id = bot.mumble.users[text.actor]['channel_id']
bot.mumble.channels[channel_id].move_in()
def cmd_user_ban(bot, user, text, command, parameter):
if bot.is_admin(user):
if parameter:
bot.mumble.users[text.actor].send_text_message(util.user_ban(parameter))
else:
bot.mumble.users[text.actor].send_text_message(util.get_user_ban())
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_user_unban(bot, user, text, command, parameter):
if bot.is_admin(user):
if parameter:
bot.mumble.users[text.actor].send_text_message(util.user_unban(parameter))
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_url_ban(bot, user, text, command, parameter):
if bot.is_admin(user):
if parameter:
bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(parameter)))
else:
bot.mumble.users[text.actor].send_text_message(util.get_url_ban())
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_url_unban(bot, user, text, command, parameter):
if bot.is_admin(user):
if parameter:
bot.mumble.users[text.actor].send_text_message(util.url_unban(util.get_url_from_input(parameter)))
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_play(bot, user, text, command, parameter):
if var.playlist.length() > 0:
if parameter is not None and parameter.isdigit() and int(parameter) > 0 \
and int(parameter) <= len(var.playlist.playlist):
bot.stop()
bot.launch_music(int(parameter) - 1)
elif bot.is_pause:
bot.resume()
else:
bot.send_msg(util.format_current_playing(), text)
else:
bot.send_msg(constants.strings('queue_empty'), text)
def cmd_pause(bot, user, text, command, parameter):
bot.pause()
bot.send_msg(constants.strings('paused'))
def cmd_play_file(bot, user, text, command, parameter):
music_folder = var.config.get('bot', 'music_folder')
# if parameter is {index}
if parameter.isdigit():
files = util.get_recursive_filelist_sorted(music_folder)
if int(parameter) < len(files):
filename = files[int(parameter)].replace(music_folder, '')
music = {'type': 'file',
'path': filename,
'user': user}
logging.info("cmd: add to playlist: " + filename)
music = var.playlist.append(music)
bot.send_msg(constants.strings('file_added', item=util.format_song_string(music)), text)
# if parameter is {path}
else:
# sanitize "../" and so on
path = os.path.abspath(os.path.join(music_folder, parameter))
if not path.startswith(os.path.abspath(music_folder)):
bot.send_msg(constants.strings('no_file'), text)
return
if os.path.isfile(path):
music = {'type': 'file',
'path': parameter,
'user': user}
logging.info("cmd: add to playlist: " + parameter)
music = var.playlist.append(music)
bot.send_msg(constants.strings('file_added', item=util.format_song_string(music)), text)
return
# if parameter is {folder}
elif os.path.isdir(path):
if parameter != '.' and parameter != './':
if not parameter.endswith("/"):
parameter += "/"
else:
parameter = ""
files = util.get_recursive_filelist_sorted(music_folder)
music_library = util.Dir(music_folder)
for file in files:
music_library.add_file(file)
files = music_library.get_files(parameter)
msgs = [constants.strings('multiple_file_added')]
count = 0
for file in files:
count += 1
music = {'type': 'file',
'path': file,
'user': user}
logging.info("cmd: add to playlist: " + file)
music = var.playlist.append(music)
msgs.append("{} ({})".format(music['title'], music['path']))
if count != 0:
send_multi_lines(bot, msgs, text)
else:
bot.send_msg(constants.strings('no_file'), text)
else:
# try to do a partial match
files = util.get_recursive_filelist_sorted(music_folder)
matches = [(index, file) for index, file in enumerate(files) if parameter.lower() in file.lower()]
if len(matches) == 0:
bot.send_msg(constants.strings('no_file'), text)
elif len(matches) == 1:
music = {'type': 'file',
'path': matches[0][1],
'user': user}
logging.info("cmd: add to playlist: " + matches[0][1])
music = var.playlist.append(music)
bot.send_msg(constants.strings('file_added', item=util.format_song_string(music)), text)
else:
msgs = [ constants.strings('multiple_matches')]
for match in matches:
msgs.append("<b>{:0>3d}</b> - {:s}".format(match[0], match[1]))
send_multi_lines(bot, msgs, text)
def cmd_play_file_match(bot, user, text, command, parameter):
music_folder = var.config.get('bot', 'music_folder')
if parameter is not None:
files = util.get_recursive_filelist_sorted(music_folder)
msgs = [ constants.strings('file_added')]
count = 0
try:
for file in files:
match = re.search(parameter, file)
if match:
count += 1
music = {'type': 'file',
'path': file,
'user': user}
logging.info("cmd: add to playlist: " + file)
music = var.playlist.append(music)
msgs.append("{} ({})".format(music['title'], music['path']))
if count != 0:
send_multi_lines(bot, msgs, text)
else:
bot.send_msg(constants.strings('no_file'), text)
except re.error as e:
msg = constants.strings('wrong_pattern', error=str(e))
bot.send_msg(msg, text)
else:
bot.send_msg(constants.strings('bad_parameter', command))
def cmd_play_url(bot, user, text, command, parameter):
music = {'type': 'url',
# grab the real URL
'url': util.get_url_from_input(parameter),
'user': user,
'ready': 'validation'}
if media.url.get_url_info(music):
if music['duration'] > var.config.getint('bot', 'max_track_duration'):
bot.send_msg(constants.strings('too_long'), text)
else:
music['ready'] = "no"
var.playlist.append(music)
logging.info("cmd: add to playlist: " + music['url'])
bot.async_download_next()
else:
bot.send_msg(constants.strings('unable_download'), text)
def cmd_play_playlist(bot, user, text, command, parameter):
offset = 0 # if you want to start the playlist at a specific index
try:
offset = int(parameter.split(" ")[-1])
except ValueError:
pass
url = util.get_url_from_input(parameter)
logging.debug("cmd: fetching media info from playlist url %s" % url)
items = media.playlist.get_playlist_info(url=url, start_index=offset, user=user)
if len(items) > 0:
var.playlist.extend(items)
for music in items:
logging.info("cmd: add to playlist: " + util.format_debug_song_string(music))
def cmd_play_radio(bot, user, text, command, parameter):
if not parameter:
all_radio = var.config.items('radio')
msg = constants.strings('preconfigurated_radio')
for i in all_radio:
comment = ""
if len(i[1].split(maxsplit=1)) == 2:
comment = " - " + i[1].split(maxsplit=1)[1]
msg += "<br />" + i[0] + comment
bot.send_msg(msg, text)
else:
if var.config.has_option('radio', command, parameter):
parameter = var.config.get('radio', parameter)
parameter = parameter.split()[0]
url = util.get_url_from_input(parameter)
if url:
music = {'type': 'radio',
'url': url,
'user': user}
var.playlist.append(music)
logging.info("cmd: add to playlist: " + music['url'])
bot.async_download_next()
else:
bot.send_msg(constants.strings('bad_url'))
def cmd_rb_query(bot, user, text, command, parameter):
logging.info('cmd: Querying radio stations')
if not parameter:
logging.debug('rbquery without parameter')
msg = constants.strings('rb_query_empty')
bot.send_msg(msg, text)
else:
logging.debug('cmd: Found query parameter: ' + parameter)
# bot.send_msg('Searching for stations - this may take some seconds...', text)
rb_stations = radiobrowser.getstations_byname(parameter)
msg = constants.strings('rb_query_result')
msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th></tr>'
if not rb_stations:
logging.debug('cmd: No matches found for rbquery ' + parameter)
bot.send_msg('Radio-Browser found no matches for ' + parameter, text)
else:
for s in rb_stations:
stationid = s['id']
stationname = s['stationname']
country = s['country']
codec = s['codec']
bitrate = s['bitrate']
genre = s['genre']
# msg += f'<tr><td>{stationid}</td><td>{stationname}</td><td>{genre}</td><td>{codec}/{bitrate}</td><td>{country}</td></tr>'
msg += '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s/%s</td><td>%s</td></tr>' % (
stationid, stationname, genre, codec, bitrate, country)
msg += '</table>'
# Full message as html table
if len(msg) <= 5000:
bot.send_msg(msg, text)
# Shorten message if message too long (stage I)
else:
logging.debug('Result too long stage I')
msg = constants.strings('rb_query_result') + ' (shortened L1)'
msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th></tr>'
for s in rb_stations:
stationid = s['id']
stationname = s['stationname']
# msg += f'<tr><td>{stationid}</td><td>{stationname}</td>'
msg += '<tr><td>%s</td><td>%s</td>' % (stationid, stationname)
msg += '</table>'
if len(msg) <= 5000:
bot.send_msg(msg, text)
# Shorten message if message too long (stage II)
else:
logging.debug('Result too long stage II')
msg = constants.strings('rb_query_result') + ' (shortened L2)'
msg += '!rbplay ID - Station Name'
for s in rb_stations:
stationid = s['id']
stationname = s['stationname'][:12]
# msg += f'{stationid} - {stationname}'
msg += '%s - %s' % (stationid, stationname)
if len(msg) <= 5000:
bot.send_msg(msg, text)
# Message still too long
else:
bot.send_msg('Query result too long to post (> 5000 characters), please try another query.',
text)
def cmd_rb_play(bot, user, text, command, parameter):
logging.debug('cmd: Play a station by ID')
if not parameter:
logging.debug('rbplay without parameter')
msg = constants.strings('rb_play_empty')
bot.send_msg(msg, text)
else:
logging.debug('cmd: Retreiving url for station ID ' + parameter)
rstation = radiobrowser.getstationname_byid(parameter)
stationname = rstation[0]['name']
country = rstation[0]['country']
codec = rstation[0]['codec']
bitrate = rstation[0]['bitrate']
genre = rstation[0]['tags']
homepage = rstation[0]['homepage']
msg = 'Radio station added to playlist:'
# msg += '<table><tr><th>ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th><th>Homepage</th></tr>' + \
# f'<tr><td>{parameter}</td><td>{stationname}</td><td>{genre}</td><td>{codec}/{bitrate}</td><td>{country}</td><td>{homepage}</td></tr></table>'
msg += '<table><tr><th>ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th><th>Homepage</th></tr>' + \
'<tr><td>%s</td><td>%s</td><td>%s</td><td>%s/%s</td><td>%s</td><td>%s</td></tr></table>' \
% (parameter, stationname, genre, codec, bitrate, country, homepage)
logging.debug('cmd: Added station to playlist %s' % stationname)
bot.send_msg(msg, text)
url = radiobrowser.geturl_byid(parameter)
if url != "-1":
logging.info('cmd: Found url: ' + url)
music = {'type': 'radio',
'title': stationname,
'artist': homepage,
'url': url,
'user': user}
var.playlist.append(music)
logging.info("cmd: add to playlist: " + music['url'])
bot.async_download_next()
else:
logging.info('cmd: No playable url found.')
msg += "No playable url found for this station, please try another station."
bot.send_msg(msg, text)
def cmd_help(bot, user, text, command, parameter):
bot.send_msg(constants.strings('help'), text)
if bot.is_admin(user):
bot.send_msg(constants.strings('admin_help'), text)
def cmd_stop(bot, user, text, command, parameter):
bot.stop()
bot.send_msg(constants.strings('stopped'), text)
def cmd_clear(bot, user, text, command, parameter):
bot.clear()
bot.send_msg(constants.strings('cleared'), text)
def cmd_kill(bot, user, text, command, parameter):
if bot.is_admin(user):
bot.pause()
bot.exit = True
else:
bot.mumble.users[text.actor].send_text_message(
constants.strings('not_admin'))
def cmd_update(bot, user, text, command, parameter):
if bot.is_admin(user):
bot.mumble.users[text.actor].send_text_message(
constants.strings('start_updating'))
msg = util.update(bot.version)
bot.mumble.users[text.actor].send_text_message(msg)
else:
bot.mumble.users[text.actor].send_text_message(
constants.strings('not_admin'))
def cmd_stop_and_getout(bot, user, text, command, parameter):
bot.stop()
if bot.channel:
bot.mumble.channels.find_by_name(bot.channel).move_in()
def cmd_volume(bot, user, text, command, parameter):
# The volume is a percentage
if parameter is not None and parameter.isdigit() and 0 <= int(parameter) <= 100:
bot.volume_set = float(float(parameter) / 100)
bot.send_msg(constants.strings('change_volume',
volume=int(bot.volume_set * 100), user=bot.mumble.users[text.actor]['name']), text)
var.db.set('bot', 'volume', str(bot.volume_set))
logging.info('cmd: volume set to %d' % (bot.volume_set * 100))
else:
bot.send_msg(constants.strings('current_volume', volume=int(bot.volume_set * 100)), text)
def cmd_ducking(bot, user, text, command, parameter):
if parameter == "" or parameter == "on":
bot.is_ducking = True
var.db.set('bot', 'ducking', True)
bot.ducking_volume = var.config.getfloat("bot", "ducking_volume", fallback=0.05)
bot.ducking_threshold = var.config.getint("bot", "ducking_threshold", fallback=5000)
bot.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_SOUNDRECEIVED,
bot.ducking_sound_received)
bot.mumble.set_receive_sound(True)
logging.info('cmd: ducking is on')
msg = "Ducking on."
bot.send_msg(msg, text)
elif parameter == "off":
bot.is_ducking = False
bot.mumble.set_receive_sound(False)
var.db.set('bot', 'ducking', False)
msg = "Ducking off."
logging.info('cmd: ducking is off')
bot.send_msg(msg, text)
def cmd_ducking_threshold(bot, user, text, command, parameter):
if parameter is not None and parameter.isdigit():
bot.ducking_threshold = int(parameter)
var.db.set('bot', 'ducking_threshold', str(bot.ducking_threshold))
msg = "Ducking threshold set to %d." % bot.ducking_threshold
bot.send_msg(msg, text)
else:
msg = "Current ducking threshold is %d." % bot.ducking_threshold
bot.send_msg(msg, text)
def cmd_ducking_volume(bot, user, text, command, parameter):
# The volume is a percentage
if parameter is not None and parameter.isdigit() and 0 <= int(parameter) <= 100:
bot.ducking_volume = float(float(parameter) / 100)
bot.send_msg(constants.strings('change_ducking_volume',
volume=int(bot.ducking_volume * 100), user=bot.mumble.users[text.actor]['name']), text)
# var.db.set('bot', 'volume', str(bot.volume_set))
var.db.set('bot', 'ducking_volume', str(bot.ducking_volume))
logging.info('cmd: volume on ducking set to %d' % (bot.ducking_volume * 100))
else:
bot.send_msg(constants.strings('current_ducking_volume', volume=int(bot.ducking_volume * 100)), text)
def cmd_current_music(bot, user, text, command, parameter):
reply = ""
if var.playlist.length() > 0:
bot.send_msg(util.format_current_playing())
else:
reply = constants.strings('not_playing')
bot.send_msg(reply, text)
def cmd_skip(bot, user, text, command, parameter):
if bot.next(): # Is no number send, just skip the current music
bot.launch_music()
bot.async_download_next()
else:
bot.send_msg(constants.strings('queue_empty'), text)
def cmd_remove(bot, user, text, command, parameter):
# Allow to remove specific music into the queue with a number
if parameter is not None and parameter.isdigit() and int(parameter) > 0 \
and int(parameter) <= var.playlist.length():
index = int(parameter) - 1
removed = None
if index == var.playlist.current_index:
removed = var.playlist.remove(index)
var.botamusique.stop()
var.botamusique.launch_music(index)
else:
removed = var.playlist.remove(index)
# the Title isn't here if the music wasn't downloaded
bot.send_msg(constants.strings('removing_item',
item=removed['title'] if 'title' in removed else removed['url']), text)
logging.info("cmd: delete from playlist: " + str(removed['path'] if 'path' in removed else removed['url']))
else:
bot.send_msg(constants.strings('bad_parameter', command=command))
def cmd_list_file(bot, user, text, command, parameter):
folder_path = var.config.get('bot', 'music_folder')
files = util.get_recursive_filelist_sorted(folder_path)
msgs = [ "<br> <b>Files available:</b>" if not parameter else "<br> <b>Matched files:</b>" ]
try:
count = 0
for index, file in enumerate(files):
if parameter:
match = re.search(parameter, file)
if not match:
continue
count += 1
msgs.append("<b>{:0>3d}</b> - {:s}".format(index, file))
if count != 0:
send_multi_lines(bot, msgs, text)
else:
bot.send_msg(constants.strings('no_file'), text)
except re.error as e:
msg = constants.strings('wrong_pattern', error=str(e))
bot.send_msg(msg, text)
def cmd_queue(bot, user, text, command, parameter):
if len(var.playlist.playlist) == 0:
msg = constants.strings('queue_empty')
bot.send_msg(msg, text)
else:
msgs = [ constants.strings('queue_contents')]
for i, value in enumerate(var.playlist.playlist):
newline = ''
if i == var.playlist.current_index:
newline = '<b>{} ▶ ({}) {} ◀</b>'.format(i + 1, value['type'],
value['title'] if 'title' in value else value['url'])
else:
newline = '<b>{}</b> ({}) {}'.format(i + 1, value['type'],
value['title'] if 'title' in value else value['url'])
msgs.append(newline)
send_multi_lines(bot, msgs, text)
def cmd_random(bot, user, text, command, parameter):
bot.stop()
var.playlist.randomize()
bot.launch_music(0)
def cmd_drop_database(bot, user, text, command, parameter):
var.db.drop_table()
var.db = Database(var.dbfile)
bot.send_msg(constants.strings('database_dropped'), text)

View File

@ -1,20 +1,45 @@
# ========================================================
# botamusique Default Configuration File
# Version 6
# ========================================================
# WARNING:
# ******************************
# **DO NOT MODIFIED THIS FILE.**
# ******************************
#
# Please create your own configuration file, and
# ONLY ADD ITEMS YOU WANT TO MODIFY into it. Other
# items will be loaded from this file automatically.
# DO NOT DIRECTLY COPY THIS FILE.
#
# That is because this file will be overridden
# during updates. New options will be added and
# old options (like [strings]) will be updated.
# ========================================================
[server]
host = 127.0.0.1
port = 64738
password =
channel =
tokens = # example: token1,token2
# example: token1,token2
tokens =
certificate =
[bot]
username = botamusique
comment = Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun !
comment = Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!
# default volume from 0 to 1.
volume = 0.1
admin = User1;User2; # Allow user to kill the bot
# Users allowed to kill the bot, or ban URLs.
admin = User1;User2;
# Folder that stores your local songs.
music_folder = music_folder/
# Folder that stores the downloaded music.
tmp_folder = /tmp/
pip3_path = venv/bin/pip
auto_update = True
auto_check_update = True
logfile =
# in MB, 0 for no cache, -1 for unlimited size
@ -27,6 +52,10 @@ announce_current_music = True
allow_other_channel_message = False
allow_private_message = True
# If save_playlist is set True, the bot will save current
# playlist before quitting and reload it the next time it start.
save_playlist = True
# Maximum track played when a playlist is added.
max_track_playlist = 20
@ -39,6 +68,7 @@ ducking_volume = 0.05
ducking_threshold = 3000
[webinterface]
# Set enabled to True if you'd like to use the web interface to manage your playlist, upload files, etc.
enabled = False
is_web_proxified = True
listening_addr = 127.0.0.1
@ -47,16 +77,38 @@ listening_port = 8181
# Set this option to True to enable password protection for the web interface
require_auth = False
user =
password =
password =
[command]
#This it the char (only on letter) the bot will recognize as a command
command_symbol = !
#this option split username, in case you use such kind of mumo plugins https://wiki.mumble.info/wiki/Mumo#Set_Status
[debug]
# Set ffmpeg to True if you want to display DEBUG level log of ffmpeg.
ffmpeg = False
mumbleConnection = False
# This is a list of default radio stations.
[radio]
ponyville = http://192.99.131.205:8000/stream.mp3 "Here a command of !radio comment"
luna = http://radio.ponyvillelive.com:8002/stream "calm and orchestra music"
radiobrony = http://62.210.138.34:8000/live "Borny music of a friend"
celestiaradio = http://celestia.aiverse.org:8000/mp3_256
jazz = http://jazz-wr04.ice.infomaniak.ch/jazz-wr04-128.mp3 "Jazz Yeah !"
# ========================================================
# WARNING: WE DO NOT SUGGEST YOU MODIFY THE FOLLOWING
# PARTS, EXCEPT YOU KNOW WHAT YOU ARE DOING.
# ========================================================
[commands]
# This is a list of characters the bot recognizes as command prefix.
command_symbol = !:
# This option split username, in case you use such kind of mumo plugins https://wiki.mumble.info/wiki/Mumo#Set_Status
split_username_at_space = False
play_file = file
play_file = file, f
play_file_match = filematch, fm
play_url = url
play_radio = radio
play_playlist = playlist
@ -65,17 +117,22 @@ rb_query = rbquery
rb_play = rbplay
help = help
pause = pause
play = p, play
stop = stop
list = list
remove = rm
clear = clear
skip = skip
current_music = np
volume = v
current_music = np, now
volume = volume
kill = kill
stop_and_getout = oust
joinme = joinme
queue = queue
repeat = repeat
random = random
update = update
list_file = listfile
user_ban = userban
user_unban = userunban
@ -84,72 +141,91 @@ url_unban = urlunban
ducking = duck
ducking_threshold = duckthres
ducking_volume = duckv
#command to reload the ban list
reload = reload
[radio]
ponyville = http://192.99.131.205:8000/stream.mp3 "Here a command of !radio comment"
luna = http://radio.ponyvillelive.com:8002/stream "calm and orchestra music"
radiobrony = http://62.210.138.34:8000/live "Borny music of a friend"
celestiaradio = http://celestia.aiverse.org:8000/mp3_256
jazz = http://jazz-wr04.ice.infomaniak.ch/jazz-wr04-128.mp3 "Jazz Yeah !"
drop_database = dropdatabase
[strings]
current_volume = volume : %d%%
change_volume = volume : %d%% by %s
bad_command = Incorrect command
not_admin = You are not an admin !
not_playing = No music right now
bad_file = Bad file requested
current_volume = Current volume: {volume}
current_ducking_volume = Volume on ducking: {volume} by {user}
change_volume = Volume set to {volume} by {user}
change_ducking_volume = Volume on ducking set to {volume} by {user}
bad_command = {command}: command not found
bad_parameter = {command}: invalid parameter
error_executing_command = {command}: Command failed with error: {error}.
not_admin = You are not an admin!
not_playing = Nothing is playing right now
no_file = File not found
wrong_pattern = Invalid regex: {error}
file_added = Added: {item}
multiple_file_added = Multiple files added:
bad_url = Bad URL requested
preconfigurated_radio = Preconfigurated Radio available
unable_download = Error while downloading the music...
preconfigurated_radio = Preconfigurated Radio available:
unable_download = Error while downloading music...
which_command = Do you mean <br /> {commands}
multiple_matches = Track not found! Possible candidates:
queue_contents = The next items in the queue are:
queue_empty = No more music in the playlist!
now_playing = Now playing %s<br />%s
not_in_my_channel = You're not in my channel, command refused !
queue_contents = Items on the playlist:
queue_empty = Playlist is empty!
now_playing = Now playing {item}<br />{thumb}
not_in_my_channel = You're not in my channel, command refused!
pm_not_allowed = Private message aren't allowed.
too_long = This music is too long, skipping !
download_in_progress = Download of %s in progress
no_possible = it's not possible to do that
removing_item = Removing entry %s from queue
user_ban = You are ban, not allowed to do that !
url_ban = This url isn't allowed !
rb_query_result = This is the result of your query, send !rbplay 'ID' to play a station
rb_query_empty = You have to add a query text to search for a matching radio stations.
rb_play_empty = Please enter a station ID from rbquery. Example: !rbplay 96748
too_long = This music is too long, skip!
download_in_progress = Download of {item} in progress
removing_item = Removed entry {item} from playlist
user_ban = You are banned, not allowed to do that!
url_ban = This url is banned!
rb_query_result = This is the result of your query, send !rbplay 'ID' to play a station:
rb_play_empty = Please specify a radio station ID!
paused = Music paused.
stopped = Music stopped.
cleared = Playlist emptied.
database_dropped = Database dropped. All records have gone.
new_version_found = <h3>Update Available!</h3> New version of botamusique is available, send <i>!update</i> to update!
start_updating = Start updating...
help = Command available:
<br />!file [path]
<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 />!rbquery - Search http://www.radio-browser.info for a radio station
<br />!rbplay - Play a radio station from !rbquery search results (eg. !rbplay 96746)
<br />!list - display list of available tracks
<br />!queue - display items in queue
<br />!np - display the current music
<br />!skip - jump to the next music of the playlist (of remove the X items if you add a number)
<br />!stop - stop and clear the playlist
<br />!oust - stop + Go to default channel
<br />!v - get or change the volume (in %)
<br />!joinme - join your own channel
<br />!duck [on/off] - enable or disable ducking function
<br />!duckthres - set the threshold of volume to activate ducking (3000 by default)
help = <h3>Commands</h3>
<b>Control</b>
<ul>
<li> <b>!play </b> (or <b>!p</b>) [{num}] - resume from pausing / start to play (the num-th song is num is given) </li>
<li> <b>!<u>pa</u>use </b> - pause </li>
<li> <b>!<u>st</u>op </b> - stop playing </li>
<li> <b>!<u>sk</u>ip </b> - jump to the next song </li>
<li> <b>!<u>v</u>olume </b> {volume} - get or change the volume (from 0 to 100) </li>
<li> <b>!duck </b> on/off - enable or disable ducking function </li>
<li> <b>!duckv </b> - set the volume of the bot when ducking is activated </li>
<li> <b>!<u>duckt</u>hres </b> - set the threshold of volume to activate ducking (3000 by default) </li>
</ul>
<b>Playlist</b>
<ul>
<li> <b>!<u>n</u>ow </b> (or <b>!np</b>) - display the current song </li>
<li> <b>!<u>q</u>ueue </b> - display items in the playlist </li>
<li> <b>!file </b>(or <b>!f</b>) {path/folder/index/keyword} - append file to the playlist by its path or index returned by !listfile </li>
<li> <b>!<u>filem</u>atch </b>(or <b>!fm</b>) {pattern} - add all files that match regex {pattern} </li>
<li> <b>!<u>ur</u>l </b> {url} - append youtube or soundcloud music to the playlist </li>
<li> <b>!<u>playl</u>ist </b> {url} [{offset}] - append items in a youtube or soundcloud playlist, and start with the {offset}-th item </li>
<li> <b>!rm </b> {num} - remove the num-th song on the playlist </li>
<li> <b>!<u>rep</u>eat </b> [{num}] - repeat current song {num} (1 by default) times.</li>
<li> <b>!<u>ran</u>dom </b> - randomize the playlist.</li>
<li> <b>!<u>rad</u>io </b> {url} - append a radio {url} to the playlist </li>
<li> <b>!<u>rbq</u>uery </b> {keyword} - query http://www.radio-browser.info for a radio station </li>
<li> <b>!<u>rbp</u>lay </b> {id} - play a radio station with {id} (eg. !rbplay 96746) </li>
<li> <b>!<u>l</u>istfile </b> [{pattern}] - display list of available files (that match the regex pattern if {pattern} is given) </li>
<li> <b>!<u>o</u>ust </b> - stop playing and go to default channel </li>
</ul>
<b>Other</b>
<ul>
<li> <b>!<u>j</u>oinme </b> - join your own channel </li>
</ul>
admin_help = Admin command:
<br />!kill (kill the bot)
<br />!update (update the bot)
<br />!userban [user] (ban a user)
<br />!userunban [user] (unban a user)
<br />!urlban [url] (ban an url)
<br />!urlunban [url] (unban an url)
<br />!reload (reload the ban config)
admin_help = <h3>Admin command</h3>
<ul>
<li><b>!<u>k</u>ill </b> - kill the bot</li>
<li><b>!<u>up</u>date </b> - update the bot</li>
<li><b>!<u>userb</u>an </b> {user} - ban a user</li>
<li><b>!<u>useru</u>nban </b> {user} - unban a user</li>
<li><b>!<u>urlb</u>an </b> {url} - ban an url</li>
<li><b>!<u>urlu</u>nban </b> {url} - unban an url</li>
<li><b>!dropdatabase</b> - clear the entire database, YOU SHOULD KNOW WHAT YOU ARE DOING.</li>
</ul>
[debug]
ffmpeg = False
mumbleConnection = False

126
configuration.example.ini Normal file
View File

@ -0,0 +1,126 @@
# ========================================================
# botamusique Example Configuration File
# Version 6
# ========================================================
# Rename this file into configuration.ini after editing.
# Uncomment lines you needed, and carefully follow the
# instructions.
# ========================================================
# [server] section tells the bot how to connect to your murmur server.
[server]
host = 127.0.0.1
port = 64738
#password =
#channel =
#tokens = token1,token2
#certificate =
# [bot] section stores some basic settings of the behavior of the bot.
[bot]
# 'username' is the user name of the bot.
# 'comment' is the comment displayed by the bot.
#username = botamusique
#comment = Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!
# 'volume' is default volume from 0 to 1.
#volume = 0.1
# 'admin': Users allowed to kill the bot, or ban URLs. Separated by ';'
#admin = User1;User2;
# 'music_folder': Folder that stores your local songs.
#music_folder = music_folder/
# 'tmp_folder': Folder that stores the downloaded music.
# 'tmp_folder_max_size': in MB, 0 for no cache, -1 for unlimited size
# 'ignored_folders', 'ignored_files': files and folders that would be ignored during scanning.
#tmp_folder = /tmp/
#tmp_folder_max_size = 10
#ignored_folders = tmp
#ignored_files = Thumbs.db
# 'auto_check_update': check for updates every time the bot starts
#auto_check_update = True
#pip3_path = venv/bin/pip
# 'logfile': write logs into this file.
#logfile =
#announce_current_music = True
#allow_other_channel_message = False
#allow_private_message = True
# 'save_playlist': If save_playlist is set True, the bot will save current
# playlist before quitting and reload it the next time it start.
#save_playlist = True
# 'max_track_playlist': Maximum track played when a playlist is added.
#max_track_playlist = 20
# 'max_track_duration': Maximum music duration (minutes)
#max_track_duration = 60
# 'ducking': If ducking is enabled, the bot will automatically attenuate its
# volume when someone is talking.
#ducking = False
#ducking_volume = 0.05
#ducking_threshold = 3000
# [webinterface] stores settings related to the web interface.
[webinterface]
# 'enable': Set 'enabled' to True if you'd like to use the web interface to manage
# your playlist, upload files, etc.
# The web interface is disable by default for security and performance reason.
#enabled = False
#listening_addr = 127.0.0.1
#listening_port = 8181
#is_web_proxified = True
# 'required_auth': Set this to True to enable password protection for the web interface.
#require_auth = False
#user =
#password =
# [debug] stores some debug settings.
[debug]
# 'ffmpeg': Set ffmpeg to True if you want to display DEBUG level log of ffmpeg.
#ffmpeg = False
#mumbleConnection = False
# [radio] is a list of default radio stations.
[radio]
#ponyville = http://192.99.131.205:8000/stream.mp3 "Here a command of !radio comment"
#luna = http://radio.ponyvillelive.com:8002/stream "calm and orchestra music"
#radiobrony = http://62.210.138.34:8000/live "Borny music of a friend"
#celestiaradio = http://celestia.aiverse.org:8000/mp3_256
#jazz = http://jazz-wr04.ice.infomaniak.ch/jazz-wr04-128.mp3 "Jazz Yeah !"
# [commands] is settings related to user command sent via mumble message.
[commands]
# 'command_symbol' is a list of characters the bot recognizes as command prefix.
#command_symbol = !:
# 'split_username_at_space': This option split username, in case you use such kind of mumo plugins https://wiki.mumble.info/wiki/Mumo#Set_Status
#split_username_at_space = False
# You may also customize commands recognized by the bot. For a full list of commands,
# see configuration.default.ini. Copy options you want to edit into this file.
#play_file = file, f
#play_file_match = filematch, fm
# [strings] is used to compose what the bot says. You can customize them to fit in
# the style of your channel, or translate into your own language.
# For a full list of strings, please see configuration.default.ini.
# Copy options you want to edit into this file.
# Note: please keep those {placeholder} of each string in your new string.
[strings]
# Some examples are:
#current_volume = Current volume: {volume}
#current_volume = 当前音量为{volume}
#current_volume = よく聞いてね!今の音量は{volume}<br />ちゃんと覚える:大音量で耳が悪くなる!
#
#bad_command = {command}: command not found
#bad_command = {command}: 未知命令,键入'!help'以获取可用命令列表
#bad_command = {command}がなに?食べれる?おいしいでしか?

32
constants.py Normal file
View File

@ -0,0 +1,32 @@
import variables as var
def strings(option, *argv, **kwargs):
string = ""
try:
string = var.config.get("strings", option)
except KeyError as e:
raise KeyError("Missed strings in configuration file: '{string}'. ".format(string=option) +
"Please restore you configuration file back to default if necessary.")
if argv or kwargs:
try:
formatted = string.format(*argv, **kwargs)
return formatted
except KeyError as e:
raise KeyError(
"Missed placeholder {{{placeholder}}} in string '{string}'. ".format(placeholder=str(e).strip("'"), string=option) +
"Please restore you configuration file back to default if necessary.")
except TypeError as e:
raise KeyError(
"Missed placeholder in string '{string}'. ".format(string=option) +
"Please restore you configuration file back to default if necessary.")
else:
return string
def commands(command):
string = ""
try:
string = var.config.get("commands", command)
return string
except KeyError as e:
raise KeyError("Missed command in configuration file: '{string}'. ".format(string=command) +
"Please restore you configuration file back to default if necessary.")

93
database.py Normal file
View File

@ -0,0 +1,93 @@
import sqlite3
class DatabaseError(Exception):
pass
class Database:
def __init__(self, db_path):
self.db_path = db_path
# connect
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# check if table exists, or create one
tables = cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='botamusique';").fetchall()
if len(tables) == 0:
cursor.execute("CREATE TABLE botamusique (section text, option text, value text, UNIQUE(section, option))")
conn.commit()
conn.close()
def get(self, section, option, **kwargs):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
result = cursor.execute("SELECT value FROM botamusique WHERE section=? AND option=?", (section, option)).fetchall()
conn.close()
if len(result) > 0:
return result[0][0]
else:
if 'fallback' in kwargs:
return kwargs['fallback']
else:
raise DatabaseError("Item not found")
def getboolean(self, section, option, **kwargs):
return bool(int(self.get(section, option, **kwargs)))
def getfloat(self, section, option, **kwargs):
return float(self.get(section, option, **kwargs))
def getint(self, section, option, **kwargs):
return int(self.get(section, option, **kwargs))
def set(self, section, option, value):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO botamusique (section, option, value)
VALUES (?, ?, ?)
''', (section, option, value))
conn.commit()
conn.close()
def has_option(self, section, option):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
result = cursor.execute("SELECT value FROM botamusique WHERE section=? AND option=?", (section, option)).fetchall()
conn.close()
if len(result) > 0:
return True
else:
return False
def remove_option(self, section, option):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("DELETE FROM botamusique WHERE section=? AND option=?", (section, option))
conn.commit()
conn.close()
def remove_section(self, section):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("DELETE FROM botamusique WHERE section=?", (section, ))
conn.commit()
conn.close()
def items(self, section):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
results = cursor.execute("SELECT option, value FROM botamusique WHERE section=?", (section, )).fetchall()
conn.close()
return map(lambda v: (v[0], v[1]), results)
def drop_table(self):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("DROP TABLE botamusique")
conn.close()

View File

@ -14,6 +14,7 @@ import errno
import media
import logging
import time
import constants
class ReverseProxied(object):
@ -105,41 +106,39 @@ def index():
music_library=music_library,
os=os,
playlist=var.playlist,
user=var.user
user=var.user,
paused=var.botamusique.is_pause
)
@web.route("/playlist", methods=['GET'])
@requires_auth
def playlist():
if var.playlist.length() == 0:
return jsonify([render_template('playlist.html',
return jsonify({'items': [render_template('playlist.html',
m=False,
index=-1
)]
)
})
data = []
items = []
for index, item in enumerate(var.playlist.playlist):
data.append(render_template('playlist.html',
items.append(render_template('playlist.html',
index=index,
m=item,
playlist=var.playlist
)
)
return jsonify(data)
return jsonify({ 'items': items })
@web.route("/post", methods=['POST'])
@requires_auth
def post():
folder_path = var.music_folder
files = util.get_recursive_filelist_sorted(var.music_folder)
music_library = util.Dir(folder_path)
for file in files:
music_library.add_file(file)
if request.method == 'POST':
logging.debug("Post request: "+ str(request.form))
if request.form:
logging.debug("Post request: "+ str(request.form))
if 'add_file_bottom' in request.form and ".." not in request.form['add_file_bottom']:
path = var.config.get('bot', 'music_folder') + request.form['add_file_bottom']
if os.path.isfile(path):
@ -147,8 +146,8 @@ def post():
'path' : request.form['add_file_bottom'],
'title' : '',
'user' : 'Web'}
var.playlist.append(var.botamusique.get_music_tag_info(item, path))
logging.info('web: add to playlist(bottom): ' + item['path'])
item = var.playlist.append(util.get_music_tag_info(item))
logging.info('web: add to playlist(bottom): ' + util.format_debug_song_string(item))
elif 'add_file_next' in request.form and ".." not in request.form['add_file_next']:
path = var.config.get('bot', 'music_folder') + request.form['add_file_next']
@ -157,11 +156,11 @@ def post():
'path' : request.form['add_file_next'],
'title' : '',
'user' : 'Web'}
var.playlist.insert(
item = var.playlist.insert(
var.playlist.current_index + 1,
var.botamusique.get_music_tag_info(item, var.config.get('bot', 'music_folder') + item['path'])
item
)
logging.info('web: add to playlist(next): ' + item['path'])
logging.info('web: add to playlist(next): ' + util.format_debug_song_string(item))
elif ('add_folder' in request.form and ".." not in request.form['add_folder']) or ('add_folder_recursively' in request.form and ".." not in request.form['add_folder_recursively']):
try:
@ -175,50 +174,63 @@ def post():
print('folder:', folder)
if os.path.isdir(var.config.get('bot', 'music_folder') + folder):
files = util.get_recursive_filelist_sorted(var.music_folder)
music_library = util.Dir(folder_path)
for file in files:
music_library.add_file(file)
if 'add_folder_recursively' in request.form:
files = music_library.get_files_recursively(folder)
else:
files = music_library.get_files(folder)
files = list(map(lambda file: var.botamusique.get_music_tag_info({'type':'file','path': os.path.join(folder, file), 'user':'Web'}, \
var.config.get('bot', 'music_folder') + os.path.join(folder, file)), files))
files = list(map(lambda file:
{'type':'file',
'path': os.path.join(folder, file),
'user':'Web'}, files))
files = var.playlist.extend(files)
for file in files:
logging.info("web: add to playlist: %s" % util.format_debug_song_string(file))
logging.info("web: add to playlist: " + " ,".join([file['path'] for file in files]))
var.playlist.extend(files)
elif 'add_url' in request.form:
var.playlist.append({'type':'url',
music = {'type':'url',
'url': request.form['add_url'],
'user': 'Web',
'ready': 'validation'})
logging.info("web: add to playlist: " + request.form['add_url'])
media.url.get_url_info()
'ready': 'validation'}
media.url.get_url_info(music)
music = var.playlist.append(music)
logging.info("web: add to playlist: " + util.format_debug_song_string(music))
var.playlist.playlist[-1]['ready'] = "no"
elif 'add_radio' in request.form:
var.playlist.append({'type': 'radio',
music = var.playlist.append({'type': 'radio',
'path': request.form['add_radio'],
'user': "Web"})
logging.info("web: add to playlist: " + request.form['add_radio'])
logging.info("web: add to playlist: " + util.format_debug_song_string(music))
elif 'delete_music' in request.form:
music = var.playlist.playlist[int(request.form['delete_music'])]
logging.info("web: delete from playlist: " + str(music['path'] if 'path' in music else music['url']))
logging.info("web: delete from playlist: " + util.format_debug_song_string(music))
if len(var.playlist.playlist) >= int(request.form['delete_music']):
if var.playlist.current_index == int(request.form['delete_music']):
var.botamusique.pause()
if var.playlist.length() >= int(request.form['delete_music']):
if int(request.form['delete_music']) == var.playlist.current_index:
var.playlist.remove(int(request.form['delete_music']))
var.botamusique.launch_music()
var.botamusique.stop()
var.botamusique.launch_music(int(request.form['delete_music']))
else:
var.playlist.remove(int(request.form['delete_music']))
elif 'play_music' in request.form:
music = var.playlist.playlist[int(request.form['play_music'])]
logging.info("web: jump to: " + str(music['path'] if 'path' in music else music['url']))
logging.info("web: jump to: " + util.format_debug_song_string(music))
if len(var.playlist.playlist) >= int(request.form['play_music']):
var.botamusique.pause()
var.botamusique.stop()
var.botamusique.launch_music(int(request.form['play_music']))
elif 'delete_music_file' in request.form and ".." not in request.form['delete_music_file']:
@ -237,11 +249,17 @@ def post():
elif 'action' in request.form:
action = request.form['action']
if action == "randomize":
var.playlist.randomize()
elif action == "stop":
var.botamusique.pause()
elif action == "clear":
var.botamusique.stop()
var.playlist.randomize()
var.botamusique.resume()
elif action == "stop":
var.botamusique.stop()
elif action == "pause":
var.botamusique.pause()
elif action == "resume":
var.botamusique.resume()
elif action == "clear":
var.botamusique.clear()
elif action == "volume_up":
if var.botamusique.volume_set + 0.03 < 1.0:
var.botamusique.volume_set = var.botamusique.volume_set + 0.03
@ -255,7 +273,10 @@ def post():
var.botamusique.volume_set = 0
logging.info("web: volume up to %d" % (var.botamusique.volume_set * 100))
return jsonify({'ver': var.playlist.version})
if(var.playlist.length() > 0):
return jsonify({'ver': var.playlist.version, 'empty': False, 'play': not var.botamusique.is_pause})
else:
return jsonify({'ver': var.playlist.version, 'empty': True, 'play': False})
@web.route('/upload', methods=["POST"])
def upload():

View File

@ -1,6 +1,8 @@
import youtube_dl
import variables as var
import util
import random
import json
class PlayList:
playlist = []
@ -9,25 +11,35 @@ class PlayList:
def append(self, item):
self.version += 1
item = util.get_music_tag_info(item)
self.playlist.append(item)
return item
def insert(self, index, item):
self.version += 1
if index == -1:
index = self.current_index
item = util.get_music_tag_info(item)
self.playlist.insert(index, item)
if index <= self.current_index:
self.current_index += 1
return item
def length(self):
return len(self.playlist)
def extend(self, items):
self.version += 1
items = list(map(
lambda item: util.get_music_tag_info(item),
items))
self.playlist.extend(items)
return items
def next(self):
self.version += 1
@ -51,10 +63,14 @@ class PlayList:
if index == -1:
index = self.current_index
removed = self.playlist[index]
del self.playlist[index]
if self.current_index <= index:
self.next()
if self.current_index > index:
self.current_index -= 1
return removed
def current_item(self):
return self.playlist[self.current_index]
@ -81,12 +97,12 @@ class PlayList:
def randomize(self):
# current_index will lose track after shuffling, thus we take current music out before shuffling
current = self.current_item()
del self.playlist[self.current_index]
#current = self.current_item()
#del self.playlist[self.current_index]
random.shuffle(self.playlist)
self.playlist.insert(0, current)
#self.playlist.insert(0, current)
self.current_index = 0
self.version += 1
@ -95,8 +111,25 @@ class PlayList:
self.playlist = []
self.current_index = 0
def save(self):
var.db.remove_section("playlist_item")
var.db.set("playlist", "current_index", self.current_index)
for index, item in enumerate(self.playlist):
var.db.set("playlist_item", str(index), json.dumps(item))
def load(self):
current_index = var.db.getint("playlist", "current_index", fallback=-1)
if current_index == -1:
return
items = list(var.db.items("playlist_item"))
items.sort(key=lambda v: int(v[0]))
self.playlist = list(map(lambda v: json.loads(v[1]), items))
self.current_index = current_index
def get_playlist_info(url, start_index=0, user=""):
items = []
ydl_opts = {
'extract_flat': 'in_playlist'
}
@ -104,6 +137,16 @@ def get_playlist_info(url, start_index=0, user=""):
for i in range(2):
try:
info = ydl.extract_info(url, download=False)
# # if url is not a playlist but a video
# if 'entries' not in info and 'webpage_url' in info:
# music = {'type': 'url',
# 'title': info['title'],
# 'url': info['webpage_url'],
# 'user': user,
# 'ready': 'validation'}
# items.append(music)
# return items
playlist_title = info['title']
for j in range(start_index, min(len(info['entries']), start_index + var.config.getint('bot', 'max_track_playlist'))):
# Unknow String if No title into the json
@ -111,7 +154,6 @@ def get_playlist_info(url, start_index=0, user=""):
# Add youtube url if the url in the json isn't a full url
url = info['entries'][j]['url'] if info['entries'][j]['url'][0:4] == 'http' else "https://www.youtube.com/watch?v=" + info['entries'][j]['url']
# append the music to a list of futur music to play
music = {'type': 'url',
'title': title,
'url': url,
@ -120,32 +162,30 @@ def get_playlist_info(url, start_index=0, user=""):
'playlist_title': playlist_title,
'playlist_url': url,
'ready': 'validation'}
var.playlist.append(music)
except youtube_dl.utils.DownloadError:
items.append(music)
except:
pass
else:
return True
return False
return items
def get_music_info(index=0):
ydl_opts = {
'playlist_items': str(index)
}
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
for i in range(2):
try:
info = ydl.extract_info(var.playlist.playlist[index]['url'], download=False)
# Check if the Duration is longer than the config
if var.playlist[index]['current_index'] == index:
var.playlist[index]['current_duration'] = info['entries'][0]['duration'] / 60
var.playlist[index]['current_title'] = info['entries'][0]['title']
# Check if the Duration of the next music is longer than the config (async download)
elif var.playlist[index]['current_index'] == index - 1:
var.playlist[index]['next_duration'] = info['entries'][0]['duration'] / 60
var.playlist[index]['next_title'] = info['entries'][0]['title']
except youtube_dl.utils.DownloadError:
pass
else:
return True
return False
# def get_music_info(index=0):
# ydl_opts = {
# 'playlist_items': str(index)
# }
# with youtube_dl.YoutubeDL(ydl_opts) as ydl:
# for i in range(2):
# try:
# info = ydl.extract_info(var.playlist.playlist[index]['url'], download=False)
# # Check if the Duration is longer than the config
# if var.playlist[index]['current_index'] == index:
# var.playlist[index]['current_duration'] = info['entries'][0]['duration'] / 60
# var.playlist[index]['current_title'] = info['entries'][0]['title']
# # Check if the Duration of the next music is longer than the config (async download)
# elif var.playlist[index]['current_index'] == index - 1:
# var.playlist[index]['next_duration'] = info['entries'][0]['duration'] / 60
# var.playlist[index]['next_title'] = info['entries'][0]['title']
# except youtube_dl.utils.DownloadError:
# pass
# else:
# return True
# return False

View File

@ -68,6 +68,6 @@ def get_radio_title(url):
title = m.group(1)
if title:
return title.decode()
except (urllib.error.URLError, urllib.error.HTTPError):
except (urllib.error.URLError, urllib.error.HTTPError, http.client.BadStatusLine):
pass
return 'Unable to get the music title'
return 'Unknown title'

File diff suppressed because it is too large Load Diff

View File

@ -37,10 +37,10 @@
<input type="text" value="{{ subdirpath }}" name="directory" hidden>
<button type="submit" class="btn btn-primary btn-sm btn-space"><i class="fa fa-download" aria-hidden="true"></i></button>
</form>
<form method="post">
<input type="text" value="{{ subdirpath }}" name="delete_folder" hidden>
<button type="submit" class="btn btn-danger btn-sm btn-space"><i class="fas fa-trash-alt"></i></button>
</form>
<button type="submit" class="btn btn-danger btn-sm btn-space"
onclick="request('/post', {delete_folder : '{{ subdirpath }}'}, true)">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</li>
<div class="collapse multi-collapse" id="multiCollapse-{{ subdirid }}">
@ -81,10 +81,10 @@
<input type="text" value="{{ filepath }}" name="file" hidden>
<button type="submit" class="btn btn-primary btn-sm btn-space"><i class="fa fa-download" aria-hidden="true"></i></button>
</form>
<form method="post">
<input type="text" value="{{ filepath }}" name="delete_music_file" hidden>
<button type="submit" class="btn btn-danger btn-sm btn-space"><i class="fas fa-trash-alt"></i></button>
</form>
<button type="submit" class="btn btn-danger btn-sm btn-space"
onclick="request('/post', {delete_music_file : '{{ filepath }}'}, true)">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</li>
{% endfor %}
@ -124,8 +124,19 @@
onclick="request('/post', {action : 'randomize'})">
<i class="fas fa-random" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-danger btn-space"
onclick="request('/post', {action : 'stop'})">
<button type="button" id="play-btn" class="btn btn-info btn-space"
onclick="request('/post', {action : 'resume'})" disabled>
<i class="fas fa-play" aria-hidden="true"></i>
</button>
<button type="button" id="pause-btn" class="btn btn-warning btn-space"
onclick="request('/post', {action : 'pause'})" disabled>
<i class="fas fa-pause" aria-hidden="true"></i>
</button>
<button type="button" id="stop-btn" class="btn btn-danger btn-space"
onclick="request('/post', {action : 'stop'})" disabled>
<i class="fas fa-stop" aria-hidden="true"></i>
</button>
</div>
@ -208,7 +219,7 @@
<div class="card-body">
<form action="./upload" method="post" enctype="multipart/form-data">
<div class="row" style="margin-bottom: 5px;">
<div id="uploadBox" class="col-lg-8 input-group">
<div id="uploadBox" class="col-lg-7 input-group">
<div id="uploadField" style="display: flex; width: 100%">
<div class="custom-file btn-space">
<input type="file" name="file[]" class="custom-file-input" id="uploadSelectFile"
@ -227,10 +238,9 @@
<option value="{{ dir }}">
{% endfor %}
</datalist>
<button class="btn btn-outline-secondary" type="submit"
id="uploadSubmit">Upload</button>
</div>
<button class="btn btn-primary btn-space" type="submit"
id="uploadSubmit" style="margin-left: -5px;">Upload!</button>
</div>
</form>
</div>
@ -239,7 +249,7 @@
</div>
</div>
<div class="bs-docs-section">
<div class="bs-docs-section" style="margin-bottom: 150px;">
<div class="row">
<div class="col">
<div class="card">
@ -247,13 +257,12 @@
<h5 class="card-title">Add URL</h5>
</div>
<div class="card-body">
<form method="post">
<label>Add Youtube/Soundcloud URL</label>
<div class="input-group">
<input class="form-control btn-space" type="text" name="add_url">
<button type="submit" class="btn btn-primary">Add URL</button>
</div>
</form>
<label>Add Youtube/Soundcloud URL</label>
<div class="input-group">
<input class="form-control btn-space" type="text" id="add_url_input" placeholder="URL...">
<button type="submit" class="btn btn-primary"
onclick="request('/post', {add_url : $('#add_url_input')[0].value })">Add URL</button>
</div>
</div>
</div>
</div>
@ -263,13 +272,12 @@
<h5 class="card-title">Add Radio</h5>
</div>
<div class="card-body">
<form method="post">
<label>Add Radio URL</label>
<div class="input-group">
<input class="form-control btn-space" type="text" name="add_radio">
<button type="submit" class="btn btn-primary">Add Radio</button>
</div>
</form>
<label>Add Radio URL</label>
<div class="input-group">
<input class="form-control btn-space" type="text" id="add_radio_input" placeholder="Radio Address...">
<button type="submit" class="btn btn-primary"
onclick="request('/post', {add_radio : $('#add_radio_input')[0].value })">Add Radio</button>
</div>
</div>
</div>
</div>
@ -294,27 +302,35 @@
var playlist_ver = 0;
function request(url, _data){
function request(url, _data, refresh=false){
$.ajax({
type: 'POST',
url: '/post',
data : _data,
statusCode : {
200 : function(data) {
if (data.ver > playlist_ver) {
if (data.ver !== playlist_ver) {
updatePlaylist();
playlist_ver = data.ver;
}
updateControls(data.empty, data.play);
}
}
});
if(refresh){
location.reload()
}
}
function displayPlaylist(data){
// console.info(data);
$("#playlist-table tr").remove();
$.each(data, function(index, item){
var items = data.items;
$.each(items, function(index, item){
$("#playlist-table").append(item);
})
});
}
function updatePlaylist(){
@ -327,6 +343,23 @@
});
}
function updateControls(empty, play){
if(empty){
$("#play-btn").prop("disabled", true);
$("#pause-btn").prop("disabled", true);
$("#stop-btn").prop("disabled", true);
}else{
if(play){
$("#play-btn").prop("disabled", true);
$("#pause-btn").prop("disabled", false);
$("#stop-btn").prop("disabled", false);
}else{
$("#play-btn").prop("disabled", false);
$("#pause-btn").prop("disabled", true);
$("#stop-btn").prop("disabled", true);
}
}
}
// Check the version of playlist to see if update is needed.
setInterval(function(){
@ -335,10 +368,11 @@
url : '/post',
statusCode : {
200 : function(data){
if(data.ver > playlist_ver){
if(data.ver !== playlist_ver){
updatePlaylist();
playlist_ver = data.ver;
}
updateControls(data.empty, data.play);
}
}
});

View File

@ -41,7 +41,7 @@
</td>
<td>
<div class="btn-group">
<button type="button" class="btn btn-success btn-sm btn-space"
<button type="button" class="btn btn-info btn-sm btn-space"
onclick="request('/post', {play_music : '{{ index }}'})">
<i class="fa fa-play" aria-hidden="true"></i>
</button>

238
util.py
View File

@ -1,15 +1,26 @@
#!/usr/bin/python3
# coding=utf-8
import hashlib
import magic
import os
import sys
import variables as var
import constants
import zipfile
import urllib.request
import mutagen
import re
import subprocess as sp
import logging
import youtube_dl
from importlib import reload
from PIL import Image
from io import BytesIO
from sys import platform
import base64
import media
import media.radio
def get_recursive_filelist_sorted(path):
filelist = []
@ -35,6 +46,156 @@ def get_recursive_filelist_sorted(path):
return filelist
def get_music_tag_info(music, uri = ""):
if "path" in music:
if not uri:
uri = var.config.get('bot', 'music_folder') + music["path"]
if os.path.isfile(uri):
match = re.search("(.+)\.(.+)", uri)
if match is None:
return music
file_no_ext = match[1]
ext = match[2]
try:
im = None
path_thumbnail = file_no_ext + ".jpg"
if os.path.isfile(path_thumbnail):
im = Image.open(path_thumbnail)
if ext == "mp3":
# title: TIT2
# artist: TPE1, TPE2
# album: TALB
# cover artwork: APIC:
tags = mutagen.File(uri)
if 'TIT2' in tags:
music['title'] = tags['TIT2'].text[0]
if 'TPE1' in tags: # artist
music['artist'] = tags['TPE1'].text[0]
if im is None:
if "APIC:" in tags:
im = Image.open(BytesIO(tags["APIC:"].data))
elif ext == "m4a" or ext == "m4b" or ext == "mp4" or ext == "m4p":
# title: ©nam (\xa9nam)
# artist: ©ART
# album: ©alb
# cover artwork: covr
tags = mutagen.File(uri)
if '©nam' in tags:
music['title'] = tags['©nam'][0]
if '©ART' in tags: # artist
music['artist'] = tags['©ART'][0]
if im is None:
if "covr" in tags:
im = Image.open(BytesIO(tags["covr"][0]))
if im:
im.thumbnail((100, 100), Image.ANTIALIAS)
buffer = BytesIO()
im = im.convert('RGB')
im.save(buffer, format="JPEG")
music['thumbnail'] = base64.b64encode(buffer.getvalue()).decode('utf-8')
except:
pass
else:
uri = music['url']
# if nothing found
if 'title' not in music:
match = re.search("([^\.]+)\.?.*", os.path.basename(uri))
music['title'] = match[1]
return music
def format_song_string(music):
display = ''
source = music["type"]
title = music["title"] if "title" in music else "Unknown title"
artist = music["artist"] if "artist" in music else "Unknown artist"
if source == "radio":
display = "[radio] {title} from {url} by {user}".format(
title=media.radio.get_radio_title(music["url"]),
url=music["url"],
user=music["user"]
)
elif source == "url" and 'from_playlist' in music:
display = "[url] {title} (from playlist <a href=\"{url}\">{playlist}</a>) by {user}".format(
title=title,
url=music["playlist_url"],
playlist=music["playlist_title"],
user=music["user"]
)
elif source == "url":
display = "[url] <a href=\"{url}\">{title}</a> by {user}".format(
title=title,
url=music["url"],
user=music["user"]
)
elif source == "file":
display = "[file] {artist} - {title} by {user}".format(
title=title,
artist=artist,
user=music["user"]
)
return display
def format_debug_song_string(music):
display = ''
source = music["type"]
title = music["title"] if "title" in music else "??"
artist = music["artist"] if "artist" in music else "??"
if source == "radio":
display = "[radio] {url} by {user}".format(
url=music["url"],
user=music["user"]
)
elif source == "url" and 'from_playlist' in music:
display = "[url] {title} ({url}) from playlist {playlist} by {user}".format(
title=title,
url=music["url"],
playlist=music["playlist_title"],
user=music["user"]
)
elif source == "url":
display = "[url] {title} ({url}) by {user}".format(
title=title,
url=music["url"],
user=music["user"]
)
elif source == "file":
display = "[file] {artist} - {title} ({path}) by {user}".format(
title=title,
artist=artist,
path=music["path"],
user=music["user"]
)
return display
def format_current_playing():
music = var.playlist.current_item()
display = format_song_string(music)
thumbnail_html = ''
if 'thumbnail' in music:
thumbnail_html = '<img width="80" src="data:image/jpge;base64,' + \
music['thumbnail'] + '"/>'
display = (constants.strings('now_playing', item=display, thumb=thumbnail_html))
return display
# - zips all files of the given zippath (must be a directory)
# - returns the absolute path of the created zip file
# - zip file will be in the applications tmp folder (according to configuration)
@ -70,52 +231,47 @@ def zipdir(zippath, zipname_prefix=None):
return zipname
def write_db():
with open(var.dbfile, 'w') as f:
var.db.write(f)
def get_user_ban():
res = "List of ban hash"
for i in var.db.items("user_ban"):
res += "<br/>" + i[0]
return res
def new_release_version():
v = int(urllib.request.urlopen(urllib.request.Request("https://azlux.fr/botamusique/version")).read())
return v
def update(version):
v = int(urllib.request.urlopen(urllib.request.Request("https://azlux.fr/botamusique/version")).read())
v = new_release_version()
if v > version:
logging.info('New version, starting update')
logging.info('update: new version, start updating...')
tp = sp.check_output(['/usr/bin/env', 'bash', 'update.sh']).decode()
logging.debug(tp)
logging.info('Update pip librairies dependancies')
logging.info('update: update pip librairies dependancies')
tp = sp.check_output([var.config.get('bot', 'pip3_path'), 'install', '--upgrade', '-r', 'requirements.txt']).decode()
msg = "New version installed"
msg = "New version installed, please restart the bot."
else:
logging.info('Starting update youtube-dl via pip3')
logging.info('update: starting update youtube-dl via pip3')
tp = sp.check_output([var.config.get('bot', 'pip3_path'), 'install', '--upgrade', 'youtube-dl']).decode()
msg = ""
if "Requirement already up-to-date" in tp:
msg += "Youtube-dl is up-to-date"
else:
msg += "Update done : " + tp.split('Successfully installed')[1]
msg += "Update done: " + tp.split('Successfully installed')[1]
reload(youtube_dl)
msg += "<br/> Youtube-dl reloaded"
return msg
def user_ban(user):
var.db.set("user_ban", user, None)
res = "User " + user + " banned"
write_db()
return res
def user_unban(user):
var.db.remove_option("user_ban", user)
res = "Done"
write_db()
return res
@ -129,16 +285,53 @@ def get_url_ban():
def url_ban(url):
var.db.set("url_ban", url, None)
res = "url " + url + " banned"
write_db()
return res
def url_unban(url):
var.db.remove_option("url_ban", url)
res = "Done"
write_db()
return res
def pipe_no_wait(pipefd):
''' Used to fetch the STDERR of ffmpeg. pipefd is the file descriptor returned from os.pipe()'''
if platform == "linux" or platform == "linux2" or platform == "darwin":
import fcntl
import os
try:
fl = fcntl.fcntl(pipefd, fcntl.F_GETFL)
fcntl.fcntl(pipefd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
except:
print(sys.exc_info()[1])
return False
else:
return True
elif platform == "win32":
# https://stackoverflow.com/questions/34504970/non-blocking-read-on-os-pipe-on-windows
import msvcrt
import os
from ctypes import windll, byref, wintypes, GetLastError, WinError
from ctypes.wintypes import HANDLE, DWORD, POINTER, BOOL
LPDWORD = POINTER(DWORD)
PIPE_NOWAIT = wintypes.DWORD(0x00000001)
ERROR_NO_DATA = 232
SetNamedPipeHandleState = windll.kernel32.SetNamedPipeHandleState
SetNamedPipeHandleState.argtypes = [HANDLE, LPDWORD, LPDWORD, LPDWORD]
SetNamedPipeHandleState.restype = BOOL
h = msvcrt.get_osfhandle(pipefd)
res = windll.kernel32.SetNamedPipeHandleState(h, byref(PIPE_NOWAIT), None, None)
if res == 0:
print(WinError())
return False
return True
class Dir(object):
def __init__(self, path):
@ -225,3 +418,16 @@ class Dir(object):
val.render_text(ident + 1)
for file in self.files:
print('{}{}'.format(' ' * (ident + 1) * 4, file))
# Parse the html from the message to get the URL
def get_url_from_input(string):
if string.startswith('http'):
return string
p = re.compile('href="(.+?)"', re.IGNORECASE)
res = re.search(p, string)
if res:
return res.group(1)
else:
return False