]> git.0d.be Git - botaradio.git/blob - command.py
fix: missed token part for joinme
[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     bot.mumble.users.myself.move_in(
70         bot.mumble.users[text.actor]['channel_id'], token=parameter)
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', 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             if bot.is_playing and not bot.is_pause:
535                 bot.stop()
536                 bot.launch_music(index)
537         else:
538             removed = var.playlist.remove(index)
539
540         # the Title isn't here if the music wasn't downloaded
541         bot.send_msg(constants.strings('removing_item',
542             item=removed['title'] if 'title' in removed else removed['url']), text)
543
544         logging.info("cmd: delete from playlist: " + str(removed['path'] if 'path' in removed else removed['url']))
545     else:
546         bot.send_msg(constants.strings('bad_parameter', command=command))
547
548
549 def cmd_list_file(bot, user, text, command, parameter):
550     folder_path = var.config.get('bot', 'music_folder')
551
552     files = util.get_recursive_filelist_sorted(folder_path)
553     msgs = [ "<br> <b>Files available:</b>" if not parameter else "<br> <b>Matched files:</b>" ]
554     try:
555         count = 0
556         for index, file in enumerate(files):
557             if parameter:
558                 match = re.search(parameter, file)
559                 if not match:
560                     continue
561
562             count += 1
563             msgs.append("<b>{:0>3d}</b> - {:s}".format(index, file))
564
565         if count != 0:
566             send_multi_lines(bot, msgs, text)
567         else:
568             bot.send_msg(constants.strings('no_file'), text)
569
570     except re.error as e:
571         msg = constants.strings('wrong_pattern', error=str(e))
572         bot.send_msg(msg, text)
573
574
575 def cmd_queue(bot, user, text, command, parameter):
576     if len(var.playlist.playlist) == 0:
577         msg = constants.strings('queue_empty')
578         bot.send_msg(msg, text)
579     else:
580         msgs = [ constants.strings('queue_contents')]
581         for i, value in enumerate(var.playlist.playlist):
582             newline = ''
583             if i == var.playlist.current_index:
584                 newline = '<b>{} ▶ ({}) {} ◀</b>'.format(i + 1, value['type'],
585                                                            value['title'] if 'title' in value else value['url'])
586             else:
587                 newline = '<b>{}</b> ({}) {}'.format(i + 1, value['type'],
588                                                      value['title'] if 'title' in value else value['url'])
589
590             msgs.append(newline)
591
592         send_multi_lines(bot, msgs, text)
593
594
595 def cmd_random(bot, user, text, command, parameter):
596     bot.stop()
597     var.playlist.randomize()
598     bot.launch_music(0)
599
600 def cmd_drop_database(bot, user, text, command, parameter):
601     var.db.drop_table()
602     var.db = Database(var.dbfile)
603     bot.send_msg(constants.strings('database_dropped'), text)