Merge pull request #52 from elpatron68/radiobrowser

Radiobrowser
This commit is contained in:
azlux 2019-07-30 23:42:23 +02:00 committed by GitHub
commit 42a99352df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 483 additions and 75 deletions

3
.gitignore vendored
View File

@ -105,3 +105,6 @@ venv.bak/
# mypy
.mypy_cache/
configuration.ini
.vscode/settings.json
2019-07-27 22_09_08-radiobrowser.py - botamusique - Visual Studio Code.png

View File

@ -4,7 +4,8 @@ Botamusique is a mumble bot which goal is to allow users to listen music togethe
Predicted functionalities will be ones you could expect from any classic music player.
Bot the can play :
- Radio url
- Radio station from url
- Radio station from http://www.radio-browser.info API (query from > 24k stations)
- Youtube/Soundcloud URL (everything supported by youtube-dl)
- Local folder (disabled, I need to work on the web interface)
@ -80,11 +81,13 @@ you have the section :
- webinterface : basic configuration about the interface (disabled by default)
- command : you can customize the command you want for each action (if you put `help = helpme` , the bot will response to `!helpme` )
- radio : here you can have a list of default radio ( I can play a jazz radio with the command `!radio jazz`)
- rbquery : search http://www.radio-browser.info API for listed radio stations - eg: `!rbquery nora`
- rbplay : Play a specific radio station by ID (from rbquery) - eg: `!rbplay 96748`
- strings : you can customize all string the bot can say.
- debug : option to active ffmpeg or pymumble debug. (Can be very verbose)
- debug : option to activate ffmpeg or pymumble debug. (Can be very verbose)
### Contributors
If you want to participate, You're welcome to fork and pull requests Fix et new features.
If you want to participate, You're welcome to fork and pull requests (fixes and new features).
The following people joined the collaborators for a faster development, big thanks:
- @slipenbois

View File

@ -50,6 +50,9 @@ play_url = url
play_radio = radio
play_playlist = playlist
rb_query = rbquery
rb_play = rbplay
help = help
stop = stop
list = list
@ -101,12 +104,15 @@ 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 !
rbqueryresult = This is the result of your query, send !rbplay 'ID' to play a station
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

0
librb/__init__.py Normal file
View File

29
librb/radiobrowser.py Normal file
View File

@ -0,0 +1,29 @@
from librb.rbRadios import RadioBrowser
rb = RadioBrowser()
def getstations_byname(query):
results = rb.stations_byname(query)
stations = []
for st in results:
try:
station = {'stationname': st['name'], 'id':st['id'], 'codec':st['codec'], 'bitrate':st['bitrate'], 'country':st['country'], 'homepage':st['homepage'], 'genre':st['tags']}
stations.append(station)
except:
pass
return stations
def geturl_byid(id):
url = rb.playable_station(id)['url']
if url != None:
return url
else:
return "-1"
def getstationname_byid(id):
return rb.stations_byid(id)
if __name__ == "__main__":
r = getstations_byname('r.sh')
stationinfo = getstationname_byid(96748)
pass

16
librb/rbConstants.py Normal file
View File

@ -0,0 +1,16 @@
BASE_URL = "http://www.radio-browser.info/webservice/"
endpoints = {
"countries": {1: "{fmt}/countries", 2: "{fmt}/countries/{filter}"},
"codecs": {1: "{fmt}/codecs", 2: "{fmt}/codecs/{filter}"},
"states": {
1: "{fmt}/states",
2: "{fmt}/states/{filter}",
3: "{fmt}/states/{country}/{filter}",
},
"languages": {1: "{fmt}/languages", 2: "{fmt}/languages/{filter}"},
"tags": {1: "{fmt}/tags", 2: "{fmt}/tags/{filter}"},
"stations": {1: "{fmt}/stations", 3: "{fmt}/stations/{by}/{search_term}"},
"playable_station": {3: "{ver}/{fmt}/url/{station_id}"},
"station_search": {1: "{fmt}/stations/search"},
}

183
librb/rbRadios.py Normal file
View File

@ -0,0 +1,183 @@
import requests
from xml.etree import ElementTree
from urllib.parse import urljoin
from librb.rbConstants import endpoints, BASE_URL
def request(endpoint, **kwargs):
fmt = kwargs.get("format", "json")
if fmt == "xml":
content_type = "application/%s" % fmt
else:
content_type = "application/%s" % fmt
headers = {"content-type": content_type, "User-Agent": "pyradios/dev"}
params = kwargs.get("params", {})
url = BASE_URL + endpoint
resp = requests.get(url, headers=headers, params=params)
if resp.status_code == 200:
if fmt == "xml":
return resp.text
return resp.json()
return resp.raise_for_status()
class EndPointBuilder:
def __init__(self, fmt="json"):
self.fmt = fmt
self._option = None
self._endpoint = None
@property
def endpoint(self):
return endpoints[self._endpoint][self._option]
def produce_endpoint(self, **parts):
self._option = len(parts)
self._endpoint = parts["endpoint"]
parts.update({"fmt": self.fmt})
return self.endpoint.format(**parts)
class RadioBrowser:
def __init__(self, fmt="json"):
self.fmt = fmt
self.builder = EndPointBuilder(fmt=self.fmt)
def countries(self, filter=""):
endpoint = self.builder.produce_endpoint(endpoint="countries")
return request(endpoint)
def codecs(self, filter=""):
endpoint = self.builder.produce_endpoint(endpoint="codecs")
return request(endpoint)
def states(self, country="", filter=""):
endpoint = self.builder.produce_endpoint(
endpoint="states", country=country, filter=filter
)
return request(endpoint)
def languages(self, filter=""):
endpoint = self.builder.produce_endpoint(endpoint="languages", filter=filter)
return request(endpoint)
def tags(self, filter=""):
endpoint = self.builder.produce_endpoint(endpoint="tags", filter=filter)
return request(endpoint)
def stations(self, **params):
endpoint = self.builder.produce_endpoint(endpoint="stations")
kwargs = {}
if params:
kwargs.update({"params": params})
return request(endpoint, **kwargs)
def stations_byid(self, id):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="byid", search_term=id
)
return request(endpoint)
def stations_byuuid(self, uuid):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="byuuid", search_term=uuid
)
return request(endpoint)
def stations_byname(self, name):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="byname", search_term=name
)
return request(endpoint)
def stations_bynameexact(self, nameexact):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bynameexact", search_term=nameexact
)
return request(endpoint)
def stations_bycodec(self, codec):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bycodec", search_term=codec
)
return request(endpoint)
def stations_bycodecexact(self, codecexact):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bycodecexact", search_term=codecexact
)
return request(endpoint)
def stations_bycountry(self, country):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bycountry", search_term=country
)
return request(endpoint)
def stations_bycountryexact(self, countryexact):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bycountryexact", search_term=countryexact
)
return request(endpoint)
def stations_bystate(self, state):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bystate", search_term=state
)
return request(endpoint)
def stations_bystateexact(self, stateexact):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bystateexact", search_term=stateexact
)
return request(endpoint)
#
def stations_bylanguage(self, language):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bylanguage", search_term=language
)
return request(endpoint)
def stations_bylanguageexact(self, languageexact):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bylanguageexact", search_term=languageexact
)
return request(endpoint)
def stations_bytag(self, tag):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bytag", search_term=tag
)
return request(endpoint)
def stations_bytagexact(self, tagexact):
endpoint = self.builder.produce_endpoint(
endpoint="stations", by="bytagexact", search_term=tagexact
)
return request(endpoint)
def playable_station(self, station_id):
endpoint = self.builder.produce_endpoint(
endpoint="playable_station", station_id=station_id, ver="v2"
)
return request(endpoint)
def station_search(self, params, **kwargs):
# http://www.radio-browser.info/webservice#Advanced_station_search
assert isinstance(params, dict), "params is not a dict"
kwargs["params"] = params
endpoint = self.builder.produce_endpoint(endpoint="station_search")
return request(endpoint, **kwargs)

View File

@ -27,6 +27,7 @@ import media.file
import media.playlist
import media.radio
import media.system
from librb import radiobrowser
"""
FORMAT OF A MUSIC INTO THE PLAYLIST
@ -70,13 +71,16 @@ class MumbleBot:
# Set specific format for the log
FORMAT = '%(asctime)s: %(message)s'
if args.verbose:
logging.basicConfig(format=FORMAT, level=logging.DEBUG, datefmt='%Y-%m-%d %H:%M:%S')
logging.basicConfig(
format=FORMAT, level=logging.DEBUG, datefmt='%Y-%m-%d %H:%M:%S')
logging.debug("Starting in DEBUG loglevel")
elif args.quiet:
logging.basicConfig(format=FORMAT, level=logging.ERROR, datefmt='%Y-%m-%d %H:%M:%S')
logging.basicConfig(
format=FORMAT, level=logging.ERROR, datefmt='%Y-%m-%d %H:%M:%S')
logging.error("Starting in ERROR loglevel")
else:
logging.basicConfig(format=FORMAT, level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S')
logging.basicConfig(
format=FORMAT, level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S')
logging.info("Starting in INFO loglevel")
# the playlist is... a list (Surprise !!)
@ -84,7 +88,8 @@ class MumbleBot:
var.user = args.user
var.music_folder = var.config.get('bot', 'music_folder')
var.is_proxified = var.config.getboolean("webinterface", "is_web_proxified")
var.is_proxified = var.config.getboolean(
"webinterface", "is_web_proxified")
self.exit = False
self.nb_exit = 0
self.thread = None
@ -94,7 +99,8 @@ class MumbleBot:
wi_addr = var.config.get("webinterface", "listening_addr")
wi_port = var.config.getint("webinterface", "listening_port")
interface.init_proxy()
tt = threading.Thread(target=start_web_interface, args=(wi_addr, wi_port))
tt = threading.Thread(
target=start_web_interface, args=(wi_addr, wi_port))
tt.daemon = True
tt.start()
@ -131,7 +137,8 @@ class MumbleBot:
self.mumble = pymumble.Mumble(host, user=self.username, port=port, password=password, tokens=tokens,
debug=var.config.getboolean('debug', 'mumbleConnection'), certfile=certificate)
self.mumble.callbacks.set_callback("text_received", self.message_received)
self.mumble.callbacks.set_callback(
"text_received", self.message_received)
self.mumble.set_codec_profile("audio")
self.mumble.start() # start the mumble thread
@ -146,7 +153,8 @@ class MumbleBot:
# Set the CTRL+C shortcut
def ctrl_caught(self, signal, frame):
logging.info("\nSIGINT caught, quitting, {} more to kill".format(2 - self.nb_exit))
logging.info(
"\nSIGINT caught, quitting, {} more to kill".format(2 - self.nb_exit))
self.exit = True
self.stop()
if self.nb_exit > 1:
@ -181,16 +189,19 @@ class MumbleBot:
logging.info(command + ' - ' + parameter + ' by ' + user)
if command == var.config.get('command', 'joinme'):
self.mumble.users.myself.move_in(self.mumble.users[text.actor]['channel_id'], token=parameter)
self.mumble.users.myself.move_in(
self.mumble.users[text.actor]['channel_id'], token=parameter)
return
# Anti stupid guy function
if not self.is_admin(user) and not var.config.getboolean('bot', 'allow_other_channel_message') and self.mumble.users[text.actor]['channel_id'] != self.mumble.users.myself['channel_id']:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'not_in_my_channel'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'not_in_my_channel'))
return
if not self.is_admin(user) and not var.config.getboolean('bot', 'allow_private_message') and text.session:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'pm_not_allowed'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'pm_not_allowed'))
return
###
@ -198,49 +209,61 @@ class MumbleBot:
###
for i in var.db.items("user_ban"):
if user.lower() == i[0]:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'user_ban'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'user_ban'))
return
if command == var.config.get('command', 'user_ban'):
if self.is_admin(user):
if parameter:
self.mumble.users[text.actor].send_text_message(util.user_ban(parameter))
self.mumble.users[text.actor].send_text_message(
util.user_ban(parameter))
else:
self.mumble.users[text.actor].send_text_message(util.get_user_ban())
self.mumble.users[text.actor].send_text_message(
util.get_user_ban())
else:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'not_admin'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'not_admin'))
return
elif command == var.config.get('command', 'user_unban'):
if self.is_admin(user):
if parameter:
self.mumble.users[text.actor].send_text_message(util.user_unban(parameter))
self.mumble.users[text.actor].send_text_message(
util.user_unban(parameter))
else:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'not_admin'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'not_admin'))
return
elif command == var.config.get('command', 'url_ban'):
if self.is_admin(user):
if parameter:
self.mumble.users[text.actor].send_text_message(util.url_ban(self.get_url_from_input(parameter)))
self.mumble.users[text.actor].send_text_message(
util.url_ban(self.get_url_from_input(parameter)))
else:
self.mumble.users[text.actor].send_text_message(util.get_url_ban())
self.mumble.users[text.actor].send_text_message(
util.get_url_ban())
else:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'not_admin'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'not_admin'))
return
elif command == var.config.get('command', 'url_unban'):
if self.is_admin(user):
if parameter:
self.mumble.users[text.actor].send_text_message(util.url_unban(self.get_url_from_input(parameter)))
self.mumble.users[text.actor].send_text_message(
util.url_unban(self.get_url_from_input(parameter)))
else:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'not_admin'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'not_admin'))
return
if parameter:
for i in var.db.items("url_ban"):
if self.get_url_from_input(parameter.lower()) == i[0]:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'url_ban'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'url_ban'))
return
###
@ -259,16 +282,19 @@ class MumbleBot:
var.playlist.append(music)
else:
# try to do a partial match
matches = [file for file in util.get_recursive_filelist_sorted(music_folder) if parameter.lower() in file.lower()]
matches = [file for file in util.get_recursive_filelist_sorted(
music_folder) if parameter.lower() in file.lower()]
if len(matches) == 0:
self.send_msg(var.config.get('strings', 'no_file'), text)
self.send_msg(var.config.get(
'strings', 'no_file'), text)
elif len(matches) == 1:
music = {'type': 'file',
'path': matches[0],
'user': user}
var.playlist.append(music)
else:
msg = var.config.get('strings', 'multiple_matches') + '<br />'
msg = var.config.get(
'strings', 'multiple_matches') + '<br />'
msg += '<br />'.join(matches)
self.send_msg(msg, text)
else:
@ -277,7 +303,8 @@ class MumbleBot:
elif command == var.config.get('command', 'play_url') and parameter:
music = {'type': 'url',
'url': self.get_url_from_input(parameter), # grab the real URL
# grab the real URL
'url': self.get_url_from_input(parameter),
'user': user,
'ready': 'validation'}
var.playlist.append(music)
@ -285,18 +312,21 @@ class MumbleBot:
if media.url.get_url_info():
if var.playlist[-1]['duration'] > var.config.getint('bot', 'max_track_duration'):
var.playlist.pop()
self.send_msg(var.config.get('strings', 'too_long'), text)
self.send_msg(var.config.get(
'strings', 'too_long'), text)
else:
for i in var.db.options("url_ban"):
if var.playlist[-1]['url'] == i:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'url_ban'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'url_ban'))
var.playlist.pop()
return
var.playlist[-1]['ready'] = "no"
self.async_download_next()
else:
var.playlist.pop()
self.send_msg(var.config.get('strings', 'unable_download'), text)
self.send_msg(var.config.get(
'strings', 'unable_download'), text)
elif command == var.config.get('command', 'play_playlist') and parameter:
offset = 1 # if you want to start the playlist at a specific index
@ -310,7 +340,8 @@ class MumbleBot:
elif command == var.config.get('command', 'play_radio'):
if not parameter:
all_radio = var.config.items('radio')
msg = var.config.get('strings', 'preconfigurated_radio') + " :"
msg = var.config.get(
'strings', 'preconfigurated_radio') + " :"
for i in all_radio:
comment = ""
if len(i[1].split(maxsplit=1)) == 2:
@ -330,11 +361,106 @@ class MumbleBot:
self.async_download_next()
else:
self.send_msg(var.config.get('strings', 'bad_url'))
# query http://www.radio-browser.info API for a radio station
elif command == var.config.get('command', 'rb_query'):
logging.info('Querying radio stations')
if not parameter:
logging.debug('rbquery without parameter')
msg += 'You have to add a query text to search for a matching radio stations.'
self.send_msg(msg, text)
else:
logging.debug('Found query parameter: ' + parameter)
# self.send_msg('Searching for stations - this may take some seconds...', text)
rb_stations = radiobrowser.getstations_byname(parameter)
msg = var.config.get('strings', 'rbqueryresult') + " :"
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('No matches found for rbquery ' + parameter)
self.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:
self.send_msg(msg, text)
# Shorten message if message too long (stage I)
else:
logging.debug('Result too long stage I')
msg = var.config.get('strings', 'rbqueryresult') + " :" + ' (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:
self.send_msg(msg, text)
# Shorten message if message too long (stage II)
else:
logging.debug('Result too long stage II')
msg = var.config.get('strings', 'rbqueryresult') + " :" + ' (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:
self.send_msg(msg, text)
# Message still too long
else:
self.send_msg('Query result too long to post (> 5000 characters), please try another query.', text)
# Play a secific station (by id) from http://www.radio-browser.info API
elif command == var.config.get('command', 'rb_play'):
logging.debug('Play a station by ID')
if not parameter:
logging.debug('rbplay without parameter')
msg += 'Please enter a station ID from rbquery. Example: !rbplay 96748'
self.send_msg(msg, text)
else:
logging.debug('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('Added station to playlist %s' % stationname)
self.send_msg(msg, text)
url = radiobrowser.geturl_byid(parameter)
if url != "-1":
logging.info('Found url: ' + url)
music = {'type': 'radio',
'url': url,
'user': user}
var.playlist.append(music)
self.async_download_next()
else:
logging.info('No playable url found.')
msg += "No playable url found for this station, please try another station."
self.send_msg(msg, text)
elif command == var.config.get('command', 'help'):
self.send_msg(var.config.get('strings', 'help'), text)
if self.is_admin(user):
self.send_msg(var.config.get('strings', 'admin_help'), text)
self.send_msg(var.config.get(
'strings', 'admin_help'), text)
elif command == var.config.get('command', 'stop'):
self.stop()
@ -344,16 +470,19 @@ class MumbleBot:
self.stop()
self.exit = True
else:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'not_admin'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'not_admin'))
elif command == var.config.get('command', 'update'):
if self.is_admin(user):
self.mumble.users[text.actor].send_text_message("Starting the update")
self.mumble.users[text.actor].send_text_message(
"Starting the update")
# Need to be improved
msg = util.update(version)
self.mumble.users[text.actor].send_text_message(msg)
else:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'not_admin'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'not_admin'))
elif command == var.config.get('command', 'stop_and_getout'):
self.stop()
@ -368,14 +497,16 @@ class MumbleBot:
int(self.volume * 100), self.mumble.users[text.actor]['name']), text)
var.db.set('bot', 'volume', str(self.volume))
else:
self.send_msg(var.config.get('strings', 'current_volume') % int(self.volume * 100), text)
self.send_msg(var.config.get(
'strings', 'current_volume') % int(self.volume * 100), text)
elif command == var.config.get('command', 'current_music'):
if len(var.playlist) > 0:
source = var.playlist[0]["type"]
if source == "radio":
reply = "[radio] {title} on {url} by {user}".format(
title=media.radio.get_radio_title(var.playlist[0]["url"]),
title=media.radio.get_radio_title(
var.playlist[0]["url"]),
url=var.playlist[0]["title"],
user=var.playlist[0]["user"]
)
@ -405,19 +536,23 @@ class MumbleBot:
self.send_msg(reply, text)
elif command == var.config.get('command', 'skip'):
if parameter is not None and parameter.isdigit() and int(parameter) > 0: # Allow to remove specific music into the queue with a number
# Allow to remove specific music into the queue with a number
if parameter is not None and parameter.isdigit() and int(parameter) > 0:
if int(parameter) < len(var.playlist):
removed = var.playlist.pop(int(parameter))
# the Title isn't here if the music wasn't downloaded
self.send_msg(var.config.get('strings', 'removing_item') % (removed['title'] if 'title' in removed else removed['url']), text)
self.send_msg(var.config.get('strings', 'removing_item') % (
removed['title'] if 'title' in removed else removed['url']), text)
else:
self.send_msg(var.config.get('strings', 'no_possible'), text)
self.send_msg(var.config.get(
'strings', 'no_possible'), text)
elif self.next(): # Is no number send, just skip the current music
self.launch_music()
self.async_download_next()
else:
self.send_msg(var.config.get('strings', 'queue_empty'), text)
self.send_msg(var.config.get(
'strings', 'queue_empty'), text)
self.stop()
elif command == var.config.get('command', 'list'):
@ -433,10 +568,12 @@ class MumbleBot:
if len(var.playlist) <= 1:
msg = var.config.get('strings', 'queue_empty')
else:
msg = var.config.get('strings', 'queue_contents') + '<br />'
msg = var.config.get(
'strings', 'queue_contents') + '<br />'
i = 1
for value in var.playlist[1:]:
msg += '[{}] ({}) {}<br />'.format(i, value['type'], value['title'] if 'title' in value else value['url'])
msg += '[{}] ({}) {}<br />'.format(i, value['type'],
value['title'] if 'title' in value else value['url'])
i += 1
self.send_msg(msg, text)
@ -445,7 +582,8 @@ class MumbleBot:
var.playlist.append(var.playlist[0])
else:
self.mumble.users[text.actor].send_text_message(var.config.get('strings', 'bad_command'))
self.mumble.users[text.actor].send_text_message(
var.config.get('strings', 'bad_command'))
@staticmethod
def is_admin(user):
@ -472,7 +610,8 @@ class MumbleBot:
logging.debug("launch_music asked" + str(var.playlist[0]))
if var.playlist[0]["type"] == "url":
# Delete older music is the tmp folder is too big
media.system.clear_tmp_folder(var.config.get('bot', 'tmp_folder'), var.config.getint('bot', 'tmp_folder_max_size'))
media.system.clear_tmp_folder(var.config.get(
'bot', 'tmp_folder'), var.config.getint('bot', 'tmp_folder_max_size'))
# Check if the music is ready to be played
if var.playlist[0]["ready"] == "downloading":
@ -487,9 +626,11 @@ class MumbleBot:
audio = EasyID3(uri)
title = ""
if audio["title"]:
title = audio["title"][0] # take the title from the file tag
# take the title from the file tag
title = audio["title"][0]
path_thumbnail = var.playlist[0]['path'][:-4] + '.jpg' # Remove .mp3 and add .jpg
# Remove .mp3 and add .jpg
path_thumbnail = var.playlist[0]['path'][:-4] + '.jpg'
thumbnail_html = ""
if os.path.isfile(path_thumbnail):
# Create the image message
@ -498,33 +639,39 @@ class MumbleBot:
buffer = BytesIO()
im.save(buffer, format="JPEG")
thumbnail_base64 = base64.b64encode(buffer.getvalue())
thumbnail_html = '<img - src="data:image/PNG;base64,' + thumbnail_base64.decode() + '"/>'
thumbnail_html = '<img - src="data:image/PNG;base64,' + \
thumbnail_base64.decode() + '"/>'
logging.debug("Thunbail data " + thumbnail_html)
if var.config.getboolean('bot', 'announce_current_music'):
self.send_msg(var.config.get('strings', 'now_playing') % (title, thumbnail_html))
self.send_msg(var.config.get(
'strings', 'now_playing') % (title, thumbnail_html))
else:
logging.error("Error with the path during launch_music")
pass
elif var.playlist[0]["type"] == "file":
uri = var.config.get('bot', 'music_folder') + var.playlist[0]["path"]
uri = var.config.get('bot', 'music_folder') + \
var.playlist[0]["path"]
elif var.playlist[0]["type"] == "radio":
uri = var.playlist[0]["url"]
title = media.radio.get_radio_server_description(uri)
var.playlist[0]["title"] = title
if var.config.getboolean('bot', 'announce_current_music'):
self.send_msg(var.config.get('strings', 'now_playing') % (title, "URL : " + uri))
self.send_msg(var.config.get('strings', 'now_playing') %
(title, "URL : " + uri))
if var.config.getboolean('debug', 'ffmpeg'):
ffmpeg_debug = "debug"
else:
ffmpeg_debug = "warning"
command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i', uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-')
command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i',
uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-')
logging.info("FFmpeg command : " + " ".join(command))
self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480) # The ffmpeg process is a thread
# The ffmpeg process is a thread
self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480)
self.is_playing = True
def download_music(self, index):
@ -533,7 +680,8 @@ class MumbleBot:
if var.playlist[index]['duration'] > var.config.getint('bot', 'max_track_duration'):
# Check the length, useful in case of playlist, it wasn't checked before)
var.playlist.pop()
logging.info("the music " + var.playlist[index]["url"] + " has a duration of " + var.playlist[index]['duration'] + "s -- too long")
logging.info(
"the music " + var.playlist[index]["url"] + " has a duration of " + var.playlist[index]['duration'] + "s -- too long")
self.send_msg(var.config.get('strings', 'too_long'))
return
else:
@ -574,9 +722,11 @@ class MumbleBot:
'preferredquality': '192'},
{'key': 'FFmpegMetadata'}]
}
self.send_msg(var.config.get('strings', "download_in_progress") % var.playlist[index]['title'])
self.send_msg(var.config.get(
'strings', "download_in_progress") % var.playlist[index]['title'])
logging.info("Information before start downloading :" + str(var.playlist[index]))
logging.info("Information before start downloading :" +
str(var.playlist[index]))
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
for i in range(2): # Always try 2 times
try:
@ -594,7 +744,8 @@ class MumbleBot:
# Do nothing in case the next music is already downloaded
logging.info("Async download next asked")
if len(var.playlist) > 1 and var.playlist[1]['type'] == 'url' and var.playlist[1]['ready'] in ["no", "validation"]:
th = threading.Thread(target=self.download_music, kwargs={'index': 1})
th = threading.Thread(
target=self.download_music, kwargs={'index': 1})
else:
return
logging.info("Start downloading next in thread")
@ -626,7 +777,8 @@ class MumbleBot:
raw_music = self.thread.stdout.read(480)
if raw_music:
# Adjust the volume and send it to mumble
self.mumble.sound_output.add_sound(audioop.mul(raw_music, 2, self.volume))
self.mumble.sound_output.add_sound(
audioop.mul(raw_music, 2, self.volume))
else:
time.sleep(0.1)
else:
@ -680,30 +832,44 @@ def start_web_interface(addr, port):
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Bot for playing music on Mumble')
parser = argparse.ArgumentParser(
description='Bot for playing music on Mumble')
# General arguments
parser.add_argument("--config", dest='config', type=str, default='configuration.ini', help='Load configuration from this file. Default: configuration.ini')
parser.add_argument("--db", dest='db', type=str, default='db.ini', help='database file. Default db.ini')
parser.add_argument("--config", dest='config', type=str, default='configuration.ini',
help='Load configuration from this file. Default: configuration.ini')
parser.add_argument("--db", dest='db', type=str,
default='db.ini', help='database file. Default db.ini')
parser.add_argument("-q", "--quiet", dest="quiet", action="store_true", help="Only Error logs")
parser.add_argument("-v", "--verbose", dest="verbose", action="store_true", help="Show debug log")
parser.add_argument("-q", "--quiet", dest="quiet",
action="store_true", help="Only Error logs")
parser.add_argument("-v", "--verbose", dest="verbose",
action="store_true", help="Show debug log")
# Mumble arguments
parser.add_argument("-s", "--server", dest="host", type=str, help="Hostname of the Mumble server")
parser.add_argument("-u", "--user", dest="user", type=str, help="Username for the bot")
parser.add_argument("-P", "--password", dest="password", type=str, help="Server password, if required")
parser.add_argument("-T", "--tokens", dest="tokens", type=str, help="Server tokens, if required")
parser.add_argument("-p", "--port", dest="port", type=int, help="Port for the Mumble server")
parser.add_argument("-c", "--channel", dest="channel", type=str, help="Default channel for the bot")
parser.add_argument("-C", "--cert", dest="certificate", type=str, default=None, help="Certificate file")
parser.add_argument("-s", "--server", dest="host",
type=str, help="Hostname of the Mumble server")
parser.add_argument("-u", "--user", dest="user",
type=str, help="Username for the bot")
parser.add_argument("-P", "--password", dest="password",
type=str, help="Server password, if required")
parser.add_argument("-T", "--tokens", dest="tokens",
type=str, help="Server tokens, if required")
parser.add_argument("-p", "--port", dest="port",
type=int, help="Port for the Mumble server")
parser.add_argument("-c", "--channel", dest="channel",
type=str, help="Default channel for the bot")
parser.add_argument("-C", "--cert", dest="certificate",
type=str, default=None, help="Certificate file")
args = parser.parse_args()
var.dbfile = args.db
config = configparser.ConfigParser(interpolation=None, allow_no_value=True)
parsed_configs = config.read(['configuration.default.ini', args.config], encoding='latin-1')
parsed_configs = config.read(
['configuration.default.ini', args.config], encoding='latin-1')
db = configparser.ConfigParser(interpolation=None, allow_no_value=True, delimiters='²')
db = configparser.ConfigParser(
interpolation=None, allow_no_value=True, delimiters='²')
db.read(var.dbfile, encoding='latin-1')
if 'url_ban' not in db.sections():
@ -714,7 +880,8 @@ if __name__ == '__main__':
db.add_section('user_ban')
if len(parsed_configs) == 0:
logging.error('Could not read configuration from file \"{}\"'.format(args.config), file=sys.stderr)
logging.error('Could not read configuration from file \"{}\"'.format(
args.config), file=sys.stderr)
sys.exit()
var.config = config

View File

@ -5,3 +5,4 @@ youtube-dl
python-magic
Pillow
mutagen
requests