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

2
.gitignore vendored
View File

@ -111,3 +111,5 @@ configuration.ini
music_folder/ 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] [server]
host = 127.0.0.1 host = 127.0.0.1
port = 64738 port = 64738
password = password =
channel = channel =
tokens = # example: token1,token2 # example: token1,token2
tokens =
certificate = certificate =
[bot] [bot]
username = botamusique 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 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/ music_folder = music_folder/
# Folder that stores the downloaded music.
tmp_folder = /tmp/ tmp_folder = /tmp/
pip3_path = venv/bin/pip pip3_path = venv/bin/pip
auto_update = True auto_check_update = True
logfile = logfile =
# in MB, 0 for no cache, -1 for unlimited size # in MB, 0 for no cache, -1 for unlimited size
@ -27,6 +52,10 @@ announce_current_music = True
allow_other_channel_message = False allow_other_channel_message = False
allow_private_message = True 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. # Maximum track played when a playlist is added.
max_track_playlist = 20 max_track_playlist = 20
@ -39,6 +68,7 @@ ducking_volume = 0.05
ducking_threshold = 3000 ducking_threshold = 3000
[webinterface] [webinterface]
# Set enabled to True if you'd like to use the web interface to manage your playlist, upload files, etc.
enabled = False enabled = False
is_web_proxified = True is_web_proxified = True
listening_addr = 127.0.0.1 listening_addr = 127.0.0.1
@ -49,14 +79,36 @@ require_auth = False
user = user =
password = password =
[command] [debug]
#This it the char (only on letter) the bot will recognize as a command # Set ffmpeg to True if you want to display DEBUG level log of ffmpeg.
command_symbol = ! ffmpeg = False
#this option split username, in case you use such kind of mumo plugins https://wiki.mumble.info/wiki/Mumo#Set_Status 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 split_username_at_space = False
play_file = file play_file = file, f
play_file_match = filematch, fm
play_url = url play_url = url
play_radio = radio play_radio = radio
play_playlist = playlist play_playlist = playlist
@ -65,17 +117,22 @@ rb_query = rbquery
rb_play = rbplay rb_play = rbplay
help = help help = help
pause = pause
play = p, play
stop = stop stop = stop
list = list remove = rm
clear = clear
skip = skip skip = skip
current_music = np current_music = np, now
volume = v volume = volume
kill = kill kill = kill
stop_and_getout = oust stop_and_getout = oust
joinme = joinme joinme = joinme
queue = queue queue = queue
repeat = repeat repeat = repeat
random = random
update = update update = update
list_file = listfile
user_ban = userban user_ban = userban
user_unban = userunban user_unban = userunban
@ -84,72 +141,91 @@ url_unban = urlunban
ducking = duck ducking = duck
ducking_threshold = duckthres ducking_threshold = duckthres
ducking_volume = duckv
#command to reload the ban list drop_database = dropdatabase
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 !"
[strings] [strings]
current_volume = volume : %d%% current_volume = Current volume: {volume}
change_volume = volume : %d%% by %s current_ducking_volume = Volume on ducking: {volume} by {user}
bad_command = Incorrect command change_volume = Volume set to {volume} by {user}
not_admin = You are not an admin ! change_ducking_volume = Volume on ducking set to {volume} by {user}
not_playing = No music right now bad_command = {command}: command not found
bad_file = Bad file requested 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 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 bad_url = Bad URL requested
preconfigurated_radio = Preconfigurated Radio available preconfigurated_radio = Preconfigurated Radio available:
unable_download = Error while downloading the music... unable_download = Error while downloading music...
which_command = Do you mean <br /> {commands}
multiple_matches = Track not found! Possible candidates: multiple_matches = Track not found! Possible candidates:
queue_contents = The next items in the queue are: queue_contents = Items on the playlist:
queue_empty = No more music in the playlist! queue_empty = Playlist is empty!
now_playing = Now playing %s<br />%s now_playing = Now playing {item}<br />{thumb}
not_in_my_channel = You're not in my channel, command refused ! not_in_my_channel = You're not in my channel, command refused!
pm_not_allowed = Private message aren't allowed. pm_not_allowed = Private message aren't allowed.
too_long = This music is too long, skipping ! too_long = This music is too long, skip!
download_in_progress = Download of %s in progress download_in_progress = Download of {item} in progress
no_possible = it's not possible to do that removing_item = Removed entry {item} from playlist
removing_item = Removing entry %s from queue user_ban = You are banned, not allowed to do that!
user_ban = You are ban, not allowed to do that ! url_ban = This url is banned!
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_result = This is the result of your query, send !rbplay 'ID' to play a station rb_play_empty = Please specify a radio station ID!
rb_query_empty = You have to add a query text to search for a matching radio stations. paused = Music paused.
rb_play_empty = Please enter a station ID from rbquery. Example: !rbplay 96748 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: help = <h3>Commands</h3>
<br />!file [path] <b>Control</b>
<br />!url [url] - youtube or soundcloud <ul>
<br />!playlist [url] [offset] - youtube or soundcloud playlist (the offset is the track number the bot will start to play - 1 by default) <li> <b>!play </b> (or <b>!p</b>) [{num}] - resume from pausing / start to play (the num-th song is num is given) </li>
<br />!radio [url] - url of a stream <li> <b>!<u>pa</u>use </b> - pause </li>
<br />!rbquery - Search http://www.radio-browser.info for a radio station <li> <b>!<u>st</u>op </b> - stop playing </li>
<br />!rbplay - Play a radio station from !rbquery search results (eg. !rbplay 96746) <li> <b>!<u>sk</u>ip </b> - jump to the next song </li>
<br />!list - display list of available tracks <li> <b>!<u>v</u>olume </b> {volume} - get or change the volume (from 0 to 100) </li>
<br />!queue - display items in queue <li> <b>!duck </b> on/off - enable or disable ducking function </li>
<br />!np - display the current music <li> <b>!duckv </b> - set the volume of the bot when ducking is activated </li>
<br />!skip - jump to the next music of the playlist (of remove the X items if you add a number) <li> <b>!<u>duckt</u>hres </b> - set the threshold of volume to activate ducking (3000 by default) </li>
<br />!stop - stop and clear the playlist </ul>
<br />!oust - stop + Go to default channel <b>Playlist</b>
<br />!v - get or change the volume (in %) <ul>
<br />!joinme - join your own channel <li> <b>!<u>n</u>ow </b> (or <b>!np</b>) - display the current song </li>
<br />!duck [on/off] - enable or disable ducking function <li> <b>!<u>q</u>ueue </b> - display items in the playlist </li>
<br />!duckthres - set the threshold of volume to activate ducking (3000 by default) <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: admin_help = <h3>Admin command</h3>
<br />!kill (kill the bot) <ul>
<br />!update (update the bot) <li><b>!<u>k</u>ill </b> - kill the bot</li>
<br />!userban [user] (ban a user) <li><b>!<u>up</u>date </b> - update the bot</li>
<br />!userunban [user] (unban a user) <li><b>!<u>userb</u>an </b> {user} - ban a user</li>
<br />!urlban [url] (ban an url) <li><b>!<u>useru</u>nban </b> {user} - unban a user</li>
<br />!urlunban [url] (unban an url) <li><b>!<u>urlb</u>an </b> {url} - ban an url</li>
<br />!reload (reload the ban config) <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 media
import logging import logging
import time import time
import constants
class ReverseProxied(object): class ReverseProxied(object):
@ -105,41 +106,39 @@ def index():
music_library=music_library, music_library=music_library,
os=os, os=os,
playlist=var.playlist, playlist=var.playlist,
user=var.user user=var.user,
paused=var.botamusique.is_pause
) )
@web.route("/playlist", methods=['GET']) @web.route("/playlist", methods=['GET'])
@requires_auth @requires_auth
def playlist(): def playlist():
if var.playlist.length() == 0: if var.playlist.length() == 0:
return jsonify([render_template('playlist.html', return jsonify({'items': [render_template('playlist.html',
m=False, m=False,
index=-1 index=-1
)] )]
) })
data = [] items = []
for index, item in enumerate(var.playlist.playlist): for index, item in enumerate(var.playlist.playlist):
data.append(render_template('playlist.html', items.append(render_template('playlist.html',
index=index, index=index,
m=item, m=item,
playlist=var.playlist playlist=var.playlist
) )
) )
return jsonify(data) return jsonify({ 'items': items })
@web.route("/post", methods=['POST']) @web.route("/post", methods=['POST'])
@requires_auth @requires_auth
def post(): def post():
folder_path = var.music_folder 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': 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']: 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'] path = var.config.get('bot', 'music_folder') + request.form['add_file_bottom']
if os.path.isfile(path): if os.path.isfile(path):
@ -147,8 +146,8 @@ def post():
'path' : request.form['add_file_bottom'], 'path' : request.form['add_file_bottom'],
'title' : '', 'title' : '',
'user' : 'Web'} 'user' : 'Web'}
var.playlist.append(var.botamusique.get_music_tag_info(item, path)) item = var.playlist.append(util.get_music_tag_info(item))
logging.info('web: add to playlist(bottom): ' + item['path']) 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']: 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'] path = var.config.get('bot', 'music_folder') + request.form['add_file_next']
@ -157,11 +156,11 @@ def post():
'path' : request.form['add_file_next'], 'path' : request.form['add_file_next'],
'title' : '', 'title' : '',
'user' : 'Web'} 'user' : 'Web'}
var.playlist.insert( item = var.playlist.insert(
var.playlist.current_index + 1, 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']): 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: try:
@ -175,50 +174,63 @@ def post():
print('folder:', folder) print('folder:', folder)
if os.path.isdir(var.config.get('bot', 'music_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: if 'add_folder_recursively' in request.form:
files = music_library.get_files_recursively(folder) files = music_library.get_files_recursively(folder)
else: else:
files = music_library.get_files(folder) 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'}, \ files = list(map(lambda file:
var.config.get('bot', 'music_folder') + os.path.join(folder, file)), files)) {'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: elif 'add_url' in request.form:
var.playlist.append({'type':'url', music = {'type':'url',
'url': request.form['add_url'], 'url': request.form['add_url'],
'user': 'Web', 'user': 'Web',
'ready': 'validation'}) 'ready': 'validation'}
logging.info("web: add to playlist: " + request.form['add_url']) media.url.get_url_info(music)
media.url.get_url_info() music = var.playlist.append(music)
logging.info("web: add to playlist: " + util.format_debug_song_string(music))
var.playlist.playlist[-1]['ready'] = "no" var.playlist.playlist[-1]['ready'] = "no"
elif 'add_radio' in request.form: elif 'add_radio' in request.form:
var.playlist.append({'type': 'radio', music = var.playlist.append({'type': 'radio',
'path': request.form['add_radio'], 'path': request.form['add_radio'],
'user': "Web"}) '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: elif 'delete_music' in request.form:
music = var.playlist.playlist[int(request.form['delete_music'])] 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.length() >= int(request.form['delete_music']):
if var.playlist.current_index == int(request.form['delete_music']): if int(request.form['delete_music']) == var.playlist.current_index:
var.botamusique.pause()
var.playlist.remove(int(request.form['delete_music'])) 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: else:
var.playlist.remove(int(request.form['delete_music'])) var.playlist.remove(int(request.form['delete_music']))
elif 'play_music' in request.form: elif 'play_music' in request.form:
music = var.playlist.playlist[int(request.form['play_music'])] 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']): 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'])) var.botamusique.launch_music(int(request.form['play_music']))
elif 'delete_music_file' in request.form and ".." not in request.form['delete_music_file']: 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: elif 'action' in request.form:
action = request.form['action'] action = request.form['action']
if action == "randomize": if action == "randomize":
var.playlist.randomize()
elif action == "stop":
var.botamusique.pause()
elif action == "clear":
var.botamusique.stop() 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": elif action == "volume_up":
if var.botamusique.volume_set + 0.03 < 1.0: if var.botamusique.volume_set + 0.03 < 1.0:
var.botamusique.volume_set = var.botamusique.volume_set + 0.03 var.botamusique.volume_set = var.botamusique.volume_set + 0.03
@ -255,7 +273,10 @@ def post():
var.botamusique.volume_set = 0 var.botamusique.volume_set = 0
logging.info("web: volume up to %d" % (var.botamusique.volume_set * 100)) 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"]) @web.route('/upload', methods=["POST"])
def upload(): def upload():

View File

@ -1,6 +1,8 @@
import youtube_dl import youtube_dl
import variables as var import variables as var
import util
import random import random
import json
class PlayList: class PlayList:
playlist = [] playlist = []
@ -9,25 +11,35 @@ class PlayList:
def append(self, item): def append(self, item):
self.version += 1 self.version += 1
item = util.get_music_tag_info(item)
self.playlist.append(item) self.playlist.append(item)
return item
def insert(self, index, item): def insert(self, index, item):
self.version += 1 self.version += 1
if index == -1: if index == -1:
index = self.current_index index = self.current_index
item = util.get_music_tag_info(item)
self.playlist.insert(index, item) self.playlist.insert(index, item)
if index <= self.current_index: if index <= self.current_index:
self.current_index += 1 self.current_index += 1
return item
def length(self): def length(self):
return len(self.playlist) return len(self.playlist)
def extend(self, items): def extend(self, items):
self.version += 1 self.version += 1
items = list(map(
lambda item: util.get_music_tag_info(item),
items))
self.playlist.extend(items) self.playlist.extend(items)
return items
def next(self): def next(self):
self.version += 1 self.version += 1
@ -51,10 +63,14 @@ class PlayList:
if index == -1: if index == -1:
index = self.current_index index = self.current_index
removed = self.playlist[index]
del self.playlist[index] del self.playlist[index]
if self.current_index <= index: if self.current_index > index:
self.next() self.current_index -= 1
return removed
def current_item(self): def current_item(self):
return self.playlist[self.current_index] return self.playlist[self.current_index]
@ -81,12 +97,12 @@ class PlayList:
def randomize(self): def randomize(self):
# current_index will lose track after shuffling, thus we take current music out before shuffling # current_index will lose track after shuffling, thus we take current music out before shuffling
current = self.current_item() #current = self.current_item()
del self.playlist[self.current_index] #del self.playlist[self.current_index]
random.shuffle(self.playlist) random.shuffle(self.playlist)
self.playlist.insert(0, current) #self.playlist.insert(0, current)
self.current_index = 0 self.current_index = 0
self.version += 1 self.version += 1
@ -95,8 +111,25 @@ class PlayList:
self.playlist = [] self.playlist = []
self.current_index = 0 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=""): def get_playlist_info(url, start_index=0, user=""):
items = []
ydl_opts = { ydl_opts = {
'extract_flat': 'in_playlist' 'extract_flat': 'in_playlist'
} }
@ -104,6 +137,16 @@ def get_playlist_info(url, start_index=0, user=""):
for i in range(2): for i in range(2):
try: try:
info = ydl.extract_info(url, download=False) 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'] playlist_title = info['title']
for j in range(start_index, min(len(info['entries']), start_index + var.config.getint('bot', 'max_track_playlist'))): 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 # 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 # 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'] 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', music = {'type': 'url',
'title': title, 'title': title,
'url': url, 'url': url,
@ -120,32 +162,30 @@ def get_playlist_info(url, start_index=0, user=""):
'playlist_title': playlist_title, 'playlist_title': playlist_title,
'playlist_url': url, 'playlist_url': url,
'ready': 'validation'} 'ready': 'validation'}
var.playlist.append(music) items.append(music)
except youtube_dl.utils.DownloadError: except:
pass pass
else:
return True
return False
return items
def get_music_info(index=0): # def get_music_info(index=0):
ydl_opts = { # ydl_opts = {
'playlist_items': str(index) # 'playlist_items': str(index)
} # }
with youtube_dl.YoutubeDL(ydl_opts) as ydl: # with youtube_dl.YoutubeDL(ydl_opts) as ydl:
for i in range(2): # for i in range(2):
try: # try:
info = ydl.extract_info(var.playlist.playlist[index]['url'], download=False) # info = ydl.extract_info(var.playlist.playlist[index]['url'], download=False)
# Check if the Duration is longer than the config # # Check if the Duration is longer than the config
if var.playlist[index]['current_index'] == index: # if var.playlist[index]['current_index'] == index:
var.playlist[index]['current_duration'] = info['entries'][0]['duration'] / 60 # var.playlist[index]['current_duration'] = info['entries'][0]['duration'] / 60
var.playlist[index]['current_title'] = info['entries'][0]['title'] # var.playlist[index]['current_title'] = info['entries'][0]['title']
# Check if the Duration of the next music is longer than the config (async download) # # Check if the Duration of the next music is longer than the config (async download)
elif var.playlist[index]['current_index'] == index - 1: # elif var.playlist[index]['current_index'] == index - 1:
var.playlist[index]['next_duration'] = info['entries'][0]['duration'] / 60 # var.playlist[index]['next_duration'] = info['entries'][0]['duration'] / 60
var.playlist[index]['next_title'] = info['entries'][0]['title'] # var.playlist[index]['next_title'] = info['entries'][0]['title']
except youtube_dl.utils.DownloadError: # except youtube_dl.utils.DownloadError:
pass # pass
else: # else:
return True # return True
return False # return False

View File

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

View File

@ -41,7 +41,7 @@
</td> </td>
<td> <td>
<div class="btn-group"> <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 }}'})"> onclick="request('/post', {play_music : '{{ index }}'})">
<i class="fa fa-play" aria-hidden="true"></i> <i class="fa fa-play" aria-hidden="true"></i>
</button> </button>

238
util.py
View File

@ -1,15 +1,26 @@
#!/usr/bin/python3 #!/usr/bin/python3
# coding=utf-8
import hashlib import hashlib
import magic import magic
import os import os
import sys
import variables as var import variables as var
import constants
import zipfile import zipfile
import urllib.request import urllib.request
import mutagen
import re
import subprocess as sp import subprocess as sp
import logging import logging
import youtube_dl import youtube_dl
from importlib import reload 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): def get_recursive_filelist_sorted(path):
filelist = [] filelist = []
@ -35,6 +46,156 @@ def get_recursive_filelist_sorted(path):
return filelist 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) # - zips all files of the given zippath (must be a directory)
# - returns the absolute path of the created zip file # - returns the absolute path of the created zip file
# - zip file will be in the applications tmp folder (according to configuration) # - zip file will be in the applications tmp folder (according to configuration)
@ -70,52 +231,47 @@ def zipdir(zippath, zipname_prefix=None):
return zipname return zipname
def write_db():
with open(var.dbfile, 'w') as f:
var.db.write(f)
def get_user_ban(): def get_user_ban():
res = "List of ban hash" res = "List of ban hash"
for i in var.db.items("user_ban"): for i in var.db.items("user_ban"):
res += "<br/>" + i[0] res += "<br/>" + i[0]
return res 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): def update(version):
v = int(urllib.request.urlopen(urllib.request.Request("https://azlux.fr/botamusique/version")).read()) v = new_release_version()
if v > 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() tp = sp.check_output(['/usr/bin/env', 'bash', 'update.sh']).decode()
logging.debug(tp) 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() 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: 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() 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]
reload(youtube_dl) reload(youtube_dl)
msg += "<br/> Youtube-dl reloaded" msg += "<br/> Youtube-dl reloaded"
return msg return msg
def user_ban(user): def user_ban(user):
var.db.set("user_ban", user, None) var.db.set("user_ban", user, None)
res = "User " + user + " banned" res = "User " + user + " banned"
write_db()
return res return res
def user_unban(user): def user_unban(user):
var.db.remove_option("user_ban", user) var.db.remove_option("user_ban", user)
res = "Done" res = "Done"
write_db()
return res return res
@ -129,16 +285,53 @@ def get_url_ban():
def url_ban(url): def url_ban(url):
var.db.set("url_ban", url, None) var.db.set("url_ban", url, None)
res = "url " + url + " banned" res = "url " + url + " banned"
write_db()
return res return res
def url_unban(url): def url_unban(url):
var.db.remove_option("url_ban", url) var.db.remove_option("url_ban", url)
res = "Done" res = "Done"
write_db()
return res 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): class Dir(object):
def __init__(self, path): def __init__(self, path):
@ -225,3 +418,16 @@ class Dir(object):
val.render_text(ident + 1) val.render_text(ident + 1)
for file in self.files: for file in self.files:
print('{}{}'.format(' ' * (ident + 1) * 4, file)) 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