]> git.0d.be Git - botaradio.git/blob - command.py
Add more controls (#71)
[botaradio.git] / command.py
1 # coding=utf-8
2 import logging
3 import os.path
4 import pymumble.pymumble_py3 as pymumble
5 import re
6
7 import constants
8 import media.file
9 import media.playlist
10 import media.radio
11 import media.system
12 import media.url
13 import util
14 import variables as var
15 from librb import radiobrowser
16 from database import Database
17
18
19 def register_all_commands(bot):
20     bot.register_command(constants.commands('joinme'), cmd_joinme)
21     bot.register_command(constants.commands('user_ban'), cmd_user_ban)
22     bot.register_command(constants.commands('user_unban'), cmd_user_unban)
23     bot.register_command(constants.commands('url_ban'), cmd_url_ban)
24     bot.register_command(constants.commands('url_unban'), cmd_url_unban)
25     bot.register_command(constants.commands('play'), cmd_play)
26     bot.register_command(constants.commands('pause'), cmd_pause)
27     bot.register_command(constants.commands('play_file'), cmd_play_file)
28     bot.register_command(constants.commands('play_file_match'), cmd_play_file_match)
29     bot.register_command(constants.commands('play_url'), cmd_play_url)
30     bot.register_command(constants.commands('play_playlist'), cmd_play_playlist)
31     bot.register_command(constants.commands('play_radio'), cmd_play_radio)
32     bot.register_command(constants.commands('rb_query'), cmd_rb_query)
33     bot.register_command(constants.commands('rb_play'), cmd_rb_play)
34     bot.register_command(constants.commands('help'), cmd_help)
35     bot.register_command(constants.commands('stop'), cmd_stop)
36     bot.register_command(constants.commands('clear'), cmd_clear)
37     bot.register_command(constants.commands('kill'), cmd_kill)
38     bot.register_command(constants.commands('update'), cmd_update)
39     bot.register_command(constants.commands('stop_and_getout'), cmd_stop_and_getout)
40     bot.register_command(constants.commands('volume'), cmd_volume)
41     bot.register_command(constants.commands('ducking'), cmd_ducking)
42     bot.register_command(constants.commands('ducking_threshold'), cmd_ducking_threshold)
43     bot.register_command(constants.commands('ducking_volume'), cmd_ducking_volume)
44     bot.register_command(constants.commands('current_music'), cmd_current_music)
45     bot.register_command(constants.commands('skip'), cmd_skip)
46     bot.register_command(constants.commands('remove'), cmd_remove)
47     bot.register_command(constants.commands('list_file'), cmd_list_file)
48     bot.register_command(constants.commands('queue'), cmd_queue)
49     bot.register_command(constants.commands('random'), cmd_random)
50     bot.register_command(constants.commands('drop_database'), cmd_drop_database)
51
52 def send_multi_lines(bot, lines, text):
53     msg = ""
54     br = ""
55     for newline in lines:
56         msg += br
57         br = "<br>"
58         if len(msg) + len(newline) > 5000:
59             bot.send_msg(msg, text)
60             msg = ""
61         msg += newline
62
63     bot.send_msg(msg, text)
64
65 # ---------------- Commands ------------------
66
67
68 def cmd_joinme(bot, user, text, command, parameter):
69     channel_id = bot.mumble.users[text.actor]['channel_id']
70     bot.mumble.channels[channel_id].move_in()
71
72
73 def cmd_user_ban(bot, user, text, command, parameter):
74     if bot.is_admin(user):
75         if parameter:
76             bot.mumble.users[text.actor].send_text_message(util.user_ban(parameter))
77         else:
78             bot.mumble.users[text.actor].send_text_message(util.get_user_ban())
79     else:
80         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
81     return
82
83
84 def cmd_user_unban(bot, user, text, command, parameter):
85     if bot.is_admin(user):
86         if parameter:
87             bot.mumble.users[text.actor].send_text_message(util.user_unban(parameter))
88     else:
89         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
90     return
91
92
93 def cmd_url_ban(bot, user, text, command, parameter):
94     if bot.is_admin(user):
95         if parameter:
96             bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(parameter)))
97         else:
98             bot.mumble.users[text.actor].send_text_message(util.get_url_ban())
99     else:
100         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
101     return
102
103
104 def cmd_url_unban(bot, user, text, command, parameter):
105     if bot.is_admin(user):
106         if parameter:
107             bot.mumble.users[text.actor].send_text_message(util.url_unban(util.get_url_from_input(parameter)))
108     else:
109         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
110     return
111
112
113 def cmd_play(bot, user, text, command, parameter):
114     if var.playlist.length() > 0:
115         if parameter is not None and parameter.isdigit() and int(parameter) > 0 \
116                 and int(parameter) <= len(var.playlist.playlist):
117             bot.stop()
118             bot.launch_music(int(parameter) - 1)
119         elif bot.is_pause:
120             bot.resume()
121         else:
122             bot.send_msg(util.format_current_playing(), text)
123     else:
124         bot.send_msg(constants.strings('queue_empty'), text)
125
126
127 def cmd_pause(bot, user, text, command, parameter):
128     bot.pause()
129     bot.send_msg(constants.strings('paused'))
130
131
132 def cmd_play_file(bot, user, text, command, parameter):
133     music_folder = var.config.get('bot', 'music_folder')
134     # if parameter is {index}
135     if parameter.isdigit():
136         files = util.get_recursive_filelist_sorted(music_folder)
137         if int(parameter) < len(files):
138             filename = files[int(parameter)].replace(music_folder, '')
139             music = {'type': 'file',
140                      'path': filename,
141                      'user': user}
142             logging.info("cmd: add to playlist: " + filename)
143             music = var.playlist.append(music)
144             bot.send_msg(constants.strings('file_added', item=util.format_song_string(music)), text)
145
146     # if parameter is {path}
147     else:
148         # sanitize "../" and so on
149         path = os.path.abspath(os.path.join(music_folder, parameter))
150         if not path.startswith(os.path.abspath(music_folder)):
151             bot.send_msg(constants.strings('no_file'), text)
152             return
153
154         if os.path.isfile(path):
155             music = {'type': 'file',
156                      'path': parameter,
157                      'user': user}
158             logging.info("cmd: add to playlist: " + parameter)
159             music = var.playlist.append(music)
160             bot.send_msg(constants.strings('file_added', item=util.format_song_string(music)), text)
161             return
162
163         # if parameter is {folder}
164         elif os.path.isdir(path):
165             if parameter != '.' and parameter != './':
166                 if not parameter.endswith("/"):
167                     parameter += "/"
168             else:
169                 parameter = ""
170
171             files = util.get_recursive_filelist_sorted(music_folder)
172             music_library = util.Dir(music_folder)
173             for file in files:
174                 music_library.add_file(file)
175
176             files = music_library.get_files(parameter)
177             msgs = [constants.strings('multiple_file_added')]
178             count = 0
179
180             for file in files:
181                 count += 1
182                 music = {'type': 'file',
183                          'path': file,
184                          'user': user}
185                 logging.info("cmd: add to playlist: " + file)
186                 music = var.playlist.append(music)
187
188                 msgs.append("{} ({})".format(music['title'], music['path']))
189
190             if count != 0:
191                 send_multi_lines(bot, msgs, text)
192             else:
193                 bot.send_msg(constants.strings('no_file'), text)
194
195         else:
196             # try to do a partial match
197             files = util.get_recursive_filelist_sorted(music_folder)
198             matches = [(index, file) for index, file in enumerate(files) if parameter.lower() in file.lower()]
199             if len(matches) == 0:
200                 bot.send_msg(constants.strings('no_file'), text)
201             elif len(matches) == 1:
202                 music = {'type': 'file',
203                          'path': matches[0][1],
204                          'user': user}
205                 logging.info("cmd: add to playlist: " + matches[0][1])
206                 music = var.playlist.append(music)
207                 bot.send_msg(constants.strings('file_added', item=util.format_song_string(music)), text)
208             else:
209                 msgs = [ constants.strings('multiple_matches')]
210                 for match in matches:
211                     msgs.append("<b>{:0>3d}</b> - {:s}".format(match[0], match[1]))
212                 send_multi_lines(bot, msgs, text)
213
214
215 def cmd_play_file_match(bot, user, text, command, parameter):
216     music_folder = var.config.get('bot', 'music_folder')
217     if parameter is not None:
218         files = util.get_recursive_filelist_sorted(music_folder)
219         msgs = [ constants.strings('file_added')]
220         count = 0
221         try:
222             for file in files:
223                 match = re.search(parameter, file)
224                 if match:
225                     count += 1
226                     music = {'type': 'file',
227                              'path': file,
228                              'user': user}
229                     logging.info("cmd: add to playlist: " + file)
230                     music = var.playlist.append(music)
231
232                     msgs.append("{} ({})".format(music['title'], music['path']))
233
234             if count != 0:
235                 send_multi_lines(bot, msgs, text)
236             else:
237                 bot.send_msg(constants.strings('no_file'), text)
238
239         except re.error as e:
240             msg = constants.strings('wrong_pattern', error=str(e))
241             bot.send_msg(msg, text)
242     else:
243         bot.send_msg(constants.strings('bad_parameter', command))
244
245
246 def cmd_play_url(bot, user, text, command, parameter):
247     music = {'type': 'url',
248              # grab the real URL
249              'url': util.get_url_from_input(parameter),
250              'user': user,
251              'ready': 'validation'}
252
253     if media.url.get_url_info(music):
254         if music['duration'] > var.config.getint('bot', 'max_track_duration'):
255             bot.send_msg(constants.strings('too_long'), text)
256         else:
257             music['ready'] = "no"
258             var.playlist.append(music)
259             logging.info("cmd: add to playlist: " + music['url'])
260             bot.async_download_next()
261     else:
262         bot.send_msg(constants.strings('unable_download'), text)
263
264
265 def cmd_play_playlist(bot, user, text, command, parameter):
266     offset = 0  # if you want to start the playlist at a specific index
267     try:
268         offset = int(parameter.split(" ")[-1])
269     except ValueError:
270         pass
271
272     url = util.get_url_from_input(parameter)
273     logging.debug("cmd: fetching media info from playlist url %s" % url)
274     items = media.playlist.get_playlist_info(url=url, start_index=offset, user=user)
275     if len(items) > 0:
276         var.playlist.extend(items)
277         for music in items:
278             logging.info("cmd: add to playlist: " + util.format_debug_song_string(music))
279
280
281 def cmd_play_radio(bot, user, text, command, parameter):
282     if not parameter:
283         all_radio = var.config.items('radio')
284         msg = constants.strings('preconfigurated_radio')
285         for i in all_radio:
286             comment = ""
287             if len(i[1].split(maxsplit=1)) == 2:
288                 comment = " - " + i[1].split(maxsplit=1)[1]
289             msg += "<br />" + i[0] + comment
290         bot.send_msg(msg, text)
291     else:
292         if var.config.has_option('radio', command, parameter):
293             parameter = var.config.get('radio', parameter)
294             parameter = parameter.split()[0]
295         url = util.get_url_from_input(parameter)
296         if url:
297             music = {'type': 'radio',
298                      'url': url,
299                      'user': user}
300             var.playlist.append(music)
301             logging.info("cmd: add to playlist: " + music['url'])
302             bot.async_download_next()
303         else:
304             bot.send_msg(constants.strings('bad_url'))
305
306
307 def cmd_rb_query(bot, user, text, command, parameter):
308     logging.info('cmd: Querying radio stations')
309     if not parameter:
310         logging.debug('rbquery without parameter')
311         msg = constants.strings('rb_query_empty')
312         bot.send_msg(msg, text)
313     else:
314         logging.debug('cmd: Found query parameter: ' + parameter)
315         # bot.send_msg('Searching for stations - this may take some seconds...', text)
316         rb_stations = radiobrowser.getstations_byname(parameter)
317         msg = constants.strings('rb_query_result')
318         msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th></tr>'
319         if not rb_stations:
320             logging.debug('cmd: No matches found for rbquery ' + parameter)
321             bot.send_msg('Radio-Browser found no matches for ' + parameter, text)
322         else:
323             for s in rb_stations:
324                 stationid = s['id']
325                 stationname = s['stationname']
326                 country = s['country']
327                 codec = s['codec']
328                 bitrate = s['bitrate']
329                 genre = s['genre']
330                 # msg += f'<tr><td>{stationid}</td><td>{stationname}</td><td>{genre}</td><td>{codec}/{bitrate}</td><td>{country}</td></tr>'
331                 msg += '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s/%s</td><td>%s</td></tr>' % (
332                     stationid, stationname, genre, codec, bitrate, country)
333             msg += '</table>'
334             # Full message as html table
335             if len(msg) <= 5000:
336                 bot.send_msg(msg, text)
337             # Shorten message if message too long (stage I)
338             else:
339                 logging.debug('Result too long stage I')
340                 msg = constants.strings('rb_query_result') + ' (shortened L1)'
341                 msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th></tr>'
342                 for s in rb_stations:
343                     stationid = s['id']
344                     stationname = s['stationname']
345                     # msg += f'<tr><td>{stationid}</td><td>{stationname}</td>'
346                     msg += '<tr><td>%s</td><td>%s</td>' % (stationid, stationname)
347                 msg += '</table>'
348                 if len(msg) <= 5000:
349                     bot.send_msg(msg, text)
350                 # Shorten message if message too long (stage II)
351                 else:
352                     logging.debug('Result too long stage II')
353                     msg = constants.strings('rb_query_result') + ' (shortened L2)'
354                     msg += '!rbplay ID - Station Name'
355                     for s in rb_stations:
356                         stationid = s['id']
357                         stationname = s['stationname'][:12]
358                         # msg += f'{stationid} - {stationname}'
359                         msg += '%s - %s' % (stationid, stationname)
360                     if len(msg) <= 5000:
361                         bot.send_msg(msg, text)
362                     # Message still too long
363                     else:
364                         bot.send_msg('Query result too long to post (> 5000 characters), please try another query.',
365                                      text)
366
367
368 def cmd_rb_play(bot, user, text, command, parameter):
369     logging.debug('cmd: Play a station by ID')
370     if not parameter:
371         logging.debug('rbplay without parameter')
372         msg = constants.strings('rb_play_empty')
373         bot.send_msg(msg, text)
374     else:
375         logging.debug('cmd: Retreiving url for station ID ' + parameter)
376         rstation = radiobrowser.getstationname_byid(parameter)
377         stationname = rstation[0]['name']
378         country = rstation[0]['country']
379         codec = rstation[0]['codec']
380         bitrate = rstation[0]['bitrate']
381         genre = rstation[0]['tags']
382         homepage = rstation[0]['homepage']
383         msg = 'Radio station added to playlist:'
384         # msg += '<table><tr><th>ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th><th>Homepage</th></tr>' + \
385         #       f'<tr><td>{parameter}</td><td>{stationname}</td><td>{genre}</td><td>{codec}/{bitrate}</td><td>{country}</td><td>{homepage}</td></tr></table>'
386         msg += '<table><tr><th>ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th><th>Homepage</th></tr>' + \
387                '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s/%s</td><td>%s</td><td>%s</td></tr></table>' \
388                % (parameter, stationname, genre, codec, bitrate, country, homepage)
389         logging.debug('cmd: Added station to playlist %s' % stationname)
390         bot.send_msg(msg, text)
391         url = radiobrowser.geturl_byid(parameter)
392         if url != "-1":
393             logging.info('cmd: Found url: ' + url)
394             music = {'type': 'radio',
395                      'title': stationname,
396                      'artist': homepage,
397                      'url': url,
398                      'user': user}
399             var.playlist.append(music)
400             logging.info("cmd: add to playlist: " + music['url'])
401             bot.async_download_next()
402         else:
403             logging.info('cmd: No playable url found.')
404             msg += "No playable url found for this station, please try another station."
405             bot.send_msg(msg, text)
406
407
408 def cmd_help(bot, user, text, command, parameter):
409     bot.send_msg(constants.strings('help'), text)
410     if bot.is_admin(user):
411         bot.send_msg(constants.strings('admin_help'), text)
412
413
414 def cmd_stop(bot, user, text, command, parameter):
415     bot.stop()
416     bot.send_msg(constants.strings('stopped'), text)
417
418
419 def cmd_clear(bot, user, text, command, parameter):
420     bot.clear()
421     bot.send_msg(constants.strings('cleared'), text)
422
423
424 def cmd_kill(bot, user, text, command, parameter):
425     if bot.is_admin(user):
426         bot.pause()
427         bot.exit = True
428     else:
429         bot.mumble.users[text.actor].send_text_message(
430             constants.strings('not_admin'))
431
432
433 def cmd_update(bot, user, text, command, parameter):
434     if bot.is_admin(user):
435         bot.mumble.users[text.actor].send_text_message(
436             constants.strings('start_updating'))
437         msg = util.update(bot.version)
438         bot.mumble.users[text.actor].send_text_message(msg)
439     else:
440         bot.mumble.users[text.actor].send_text_message(
441             constants.strings('not_admin'))
442
443
444 def cmd_stop_and_getout(bot, user, text, command, parameter):
445     bot.stop()
446     if bot.channel:
447         bot.mumble.channels.find_by_name(bot.channel).move_in()
448
449
450 def cmd_volume(bot, user, text, command, parameter):
451     # The volume is a percentage
452     if parameter is not None and parameter.isdigit() and 0 <= int(parameter) <= 100:
453         bot.volume_set = float(float(parameter) / 100)
454         bot.send_msg(constants.strings('change_volume',
455             volume=int(bot.volume_set * 100), user=bot.mumble.users[text.actor]['name']), text)
456         var.db.set('bot', 'volume', str(bot.volume_set))
457         logging.info('cmd: volume set to %d' % (bot.volume_set * 100))
458     else:
459         bot.send_msg(constants.strings('current_volume', volume=int(bot.volume_set * 100)), text)
460
461
462 def cmd_ducking(bot, user, text, command, parameter):
463     if parameter == "" or parameter == "on":
464         bot.is_ducking = True
465         var.db.set('bot', 'ducking', True)
466         bot.ducking_volume = var.config.getfloat("bot", "ducking_volume", fallback=0.05)
467         bot.ducking_threshold = var.config.getint("bot", "ducking_threshold", fallback=5000)
468         bot.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_SOUNDRECEIVED,
469                                           bot.ducking_sound_received)
470         bot.mumble.set_receive_sound(True)
471         logging.info('cmd: ducking is on')
472         msg = "Ducking on."
473         bot.send_msg(msg, text)
474     elif parameter == "off":
475         bot.is_ducking = False
476         bot.mumble.set_receive_sound(False)
477         var.db.set('bot', 'ducking', False)
478         msg = "Ducking off."
479         logging.info('cmd: ducking is off')
480         bot.send_msg(msg, text)
481
482
483 def cmd_ducking_threshold(bot, user, text, command, parameter):
484     if parameter is not None and parameter.isdigit():
485         bot.ducking_threshold = int(parameter)
486         var.db.set('bot', 'ducking_threshold', str(bot.ducking_threshold))
487         msg = "Ducking threshold set to %d." % bot.ducking_threshold
488         bot.send_msg(msg, text)
489     else:
490         msg = "Current ducking threshold is %d." % bot.ducking_threshold
491         bot.send_msg(msg, text)
492
493
494 def cmd_ducking_volume(bot, user, text, command, parameter):
495     # The volume is a percentage
496     if parameter is not None and parameter.isdigit() and 0 <= int(parameter) <= 100:
497         bot.ducking_volume = float(float(parameter) / 100)
498         bot.send_msg(constants.strings('change_ducking_volume',
499             volume=int(bot.ducking_volume * 100), user=bot.mumble.users[text.actor]['name']), text)
500         # var.db.set('bot', 'volume', str(bot.volume_set))
501         var.db.set('bot', 'ducking_volume', str(bot.ducking_volume))
502         logging.info('cmd: volume on ducking set to %d' % (bot.ducking_volume * 100))
503     else:
504         bot.send_msg(constants.strings('current_ducking_volume', volume=int(bot.ducking_volume * 100)), text)
505
506
507 def cmd_current_music(bot, user, text, command, parameter):
508     reply = ""
509     if var.playlist.length() > 0:
510         bot.send_msg(util.format_current_playing())
511     else:
512         reply = constants.strings('not_playing')
513     bot.send_msg(reply, text)
514
515
516 def cmd_skip(bot, user, text, command, parameter):
517     if bot.next():  # Is no number send, just skip the current music
518         bot.launch_music()
519         bot.async_download_next()
520     else:
521         bot.send_msg(constants.strings('queue_empty'), text)
522
523
524 def cmd_remove(bot, user, text, command, parameter):
525     # Allow to remove specific music into the queue with a number
526     if parameter is not None and parameter.isdigit() and int(parameter) > 0 \
527             and int(parameter) <= var.playlist.length():
528
529         index = int(parameter) - 1
530
531         removed = None
532         if index == var.playlist.current_index:
533             removed = var.playlist.remove(index)
534             var.botamusique.stop()
535             var.botamusique.launch_music(index)
536         else:
537             removed = var.playlist.remove(index)
538
539         # the Title isn't here if the music wasn't downloaded
540         bot.send_msg(constants.strings('removing_item',
541             item=removed['title'] if 'title' in removed else removed['url']), text)
542
543         logging.info("cmd: delete from playlist: " + str(removed['path'] if 'path' in removed else removed['url']))
544     else:
545         bot.send_msg(constants.strings('bad_parameter', command=command))
546
547
548 def cmd_list_file(bot, user, text, command, parameter):
549     folder_path = var.config.get('bot', 'music_folder')
550
551     files = util.get_recursive_filelist_sorted(folder_path)
552     msgs = [ "<br> <b>Files available:</b>" if not parameter else "<br> <b>Matched files:</b>" ]
553     try:
554         count = 0
555         for index, file in enumerate(files):
556             if parameter:
557                 match = re.search(parameter, file)
558                 if not match:
559                     continue
560
561             count += 1
562             msgs.append("<b>{:0>3d}</b> - {:s}".format(index, file))
563
564         if count != 0:
565             send_multi_lines(bot, msgs, text)
566         else:
567             bot.send_msg(constants.strings('no_file'), text)
568
569     except re.error as e:
570         msg = constants.strings('wrong_pattern', error=str(e))
571         bot.send_msg(msg, text)
572
573
574 def cmd_queue(bot, user, text, command, parameter):
575     if len(var.playlist.playlist) == 0:
576         msg = constants.strings('queue_empty')
577         bot.send_msg(msg, text)
578     else:
579         msgs = [ constants.strings('queue_contents')]
580         for i, value in enumerate(var.playlist.playlist):
581             newline = ''
582             if i == var.playlist.current_index:
583                 newline = '<b>{} ▶ ({}) {} ◀</b>'.format(i + 1, value['type'],
584                                                            value['title'] if 'title' in value else value['url'])
585             else:
586                 newline = '<b>{}</b> ({}) {}'.format(i + 1, value['type'],
587                                                      value['title'] if 'title' in value else value['url'])
588
589             msgs.append(newline)
590
591         send_multi_lines(bot, msgs, text)
592
593
594 def cmd_random(bot, user, text, command, parameter):
595     bot.stop()
596     var.playlist.randomize()
597     bot.launch_music(0)
598
599 def cmd_drop_database(bot, user, text, command, parameter):
600     var.db.drop_table()
601     var.db = Database(var.dbfile)
602     bot.send_msg(constants.strings('database_dropped'), text)