]> git.0d.be Git - botaradio.git/blob - command.py
fix: impose file path check for launch_music and resume, fixed #77
[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             if var.playlist.length() == 2:
261                 # If I am the second item on the playlist. (I am the next one!)
262                 bot.async_download_next()
263     else:
264         bot.send_msg(constants.strings('unable_download'), text)
265
266
267 def cmd_play_playlist(bot, user, text, command, parameter):
268     offset = 0  # if you want to start the playlist at a specific index
269     try:
270         offset = int(parameter.split(" ")[-1])
271     except ValueError:
272         pass
273
274     url = util.get_url_from_input(parameter)
275     logging.debug("cmd: fetching media info from playlist url %s" % url)
276     items = media.playlist.get_playlist_info(url=url, start_index=offset, user=user)
277     if len(items) > 0:
278         var.playlist.extend(items)
279         for music in items:
280             logging.info("cmd: add to playlist: " + util.format_debug_song_string(music))
281
282
283 def cmd_play_radio(bot, user, text, command, parameter):
284     if not parameter:
285         all_radio = var.config.items('radio')
286         msg = constants.strings('preconfigurated_radio')
287         for i in all_radio:
288             comment = ""
289             if len(i[1].split(maxsplit=1)) == 2:
290                 comment = " - " + i[1].split(maxsplit=1)[1]
291             msg += "<br />" + i[0] + comment
292         bot.send_msg(msg, text)
293     else:
294         if var.config.has_option('radio', parameter):
295             parameter = var.config.get('radio', parameter)
296             parameter = parameter.split()[0]
297         url = util.get_url_from_input(parameter)
298         if url:
299             music = {'type': 'radio',
300                      'url': url,
301                      'user': user}
302             var.playlist.append(music)
303             logging.info("cmd: add to playlist: " + music['url'])
304             bot.async_download_next()
305         else:
306             bot.send_msg(constants.strings('bad_url'))
307
308
309 def cmd_rb_query(bot, user, text, command, parameter):
310     logging.info('cmd: Querying radio stations')
311     if not parameter:
312         logging.debug('rbquery without parameter')
313         msg = constants.strings('rb_query_empty')
314         bot.send_msg(msg, text)
315     else:
316         logging.debug('cmd: Found query parameter: ' + parameter)
317         # bot.send_msg('Searching for stations - this may take some seconds...', text)
318         rb_stations = radiobrowser.getstations_byname(parameter)
319         msg = constants.strings('rb_query_result')
320         msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th></tr>'
321         if not rb_stations:
322             logging.debug('cmd: No matches found for rbquery ' + parameter)
323             bot.send_msg('Radio-Browser found no matches for ' + parameter, text)
324         else:
325             for s in rb_stations:
326                 stationid = s['id']
327                 stationname = s['stationname']
328                 country = s['country']
329                 codec = s['codec']
330                 bitrate = s['bitrate']
331                 genre = s['genre']
332                 # msg += f'<tr><td>{stationid}</td><td>{stationname}</td><td>{genre}</td><td>{codec}/{bitrate}</td><td>{country}</td></tr>'
333                 msg += '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s/%s</td><td>%s</td></tr>' % (
334                     stationid, stationname, genre, codec, bitrate, country)
335             msg += '</table>'
336             # Full message as html table
337             if len(msg) <= 5000:
338                 bot.send_msg(msg, text)
339             # Shorten message if message too long (stage I)
340             else:
341                 logging.debug('Result too long stage I')
342                 msg = constants.strings('rb_query_result') + ' (shortened L1)'
343                 msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th></tr>'
344                 for s in rb_stations:
345                     stationid = s['id']
346                     stationname = s['stationname']
347                     # msg += f'<tr><td>{stationid}</td><td>{stationname}</td>'
348                     msg += '<tr><td>%s</td><td>%s</td>' % (stationid, stationname)
349                 msg += '</table>'
350                 if len(msg) <= 5000:
351                     bot.send_msg(msg, text)
352                 # Shorten message if message too long (stage II)
353                 else:
354                     logging.debug('Result too long stage II')
355                     msg = constants.strings('rb_query_result') + ' (shortened L2)'
356                     msg += '!rbplay ID - Station Name'
357                     for s in rb_stations:
358                         stationid = s['id']
359                         stationname = s['stationname'][:12]
360                         # msg += f'{stationid} - {stationname}'
361                         msg += '%s - %s' % (stationid, stationname)
362                     if len(msg) <= 5000:
363                         bot.send_msg(msg, text)
364                     # Message still too long
365                     else:
366                         bot.send_msg('Query result too long to post (> 5000 characters), please try another query.',
367                                      text)
368
369
370 def cmd_rb_play(bot, user, text, command, parameter):
371     logging.debug('cmd: Play a station by ID')
372     if not parameter:
373         logging.debug('rbplay without parameter')
374         msg = constants.strings('rb_play_empty')
375         bot.send_msg(msg, text)
376     else:
377         logging.debug('cmd: Retreiving url for station ID ' + parameter)
378         rstation = radiobrowser.getstationname_byid(parameter)
379         stationname = rstation[0]['name']
380         country = rstation[0]['country']
381         codec = rstation[0]['codec']
382         bitrate = rstation[0]['bitrate']
383         genre = rstation[0]['tags']
384         homepage = rstation[0]['homepage']
385         msg = 'Radio station added to playlist:'
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         #       f'<tr><td>{parameter}</td><td>{stationname}</td><td>{genre}</td><td>{codec}/{bitrate}</td><td>{country}</td><td>{homepage}</td></tr></table>'
388         msg += '<table><tr><th>ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th><th>Homepage</th></tr>' + \
389                '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s/%s</td><td>%s</td><td>%s</td></tr></table>' \
390                % (parameter, stationname, genre, codec, bitrate, country, homepage)
391         logging.debug('cmd: Added station to playlist %s' % stationname)
392         bot.send_msg(msg, text)
393         url = radiobrowser.geturl_byid(parameter)
394         if url != "-1":
395             logging.info('cmd: Found url: ' + url)
396             music = {'type': 'radio',
397                      'title': stationname,
398                      'artist': homepage,
399                      'url': url,
400                      'user': user}
401             var.playlist.append(music)
402             logging.info("cmd: add to playlist: " + music['url'])
403             bot.async_download_next()
404         else:
405             logging.info('cmd: No playable url found.')
406             msg += "No playable url found for this station, please try another station."
407             bot.send_msg(msg, text)
408
409
410 def cmd_help(bot, user, text, command, parameter):
411     bot.send_msg(constants.strings('help'), text)
412     if bot.is_admin(user):
413         bot.send_msg(constants.strings('admin_help'), text)
414
415
416 def cmd_stop(bot, user, text, command, parameter):
417     bot.stop()
418     bot.send_msg(constants.strings('stopped'), text)
419
420
421 def cmd_clear(bot, user, text, command, parameter):
422     bot.clear()
423     bot.send_msg(constants.strings('cleared'), text)
424
425
426 def cmd_kill(bot, user, text, command, parameter):
427     if bot.is_admin(user):
428         bot.pause()
429         bot.exit = True
430     else:
431         bot.mumble.users[text.actor].send_text_message(
432             constants.strings('not_admin'))
433
434
435 def cmd_update(bot, user, text, command, parameter):
436     if bot.is_admin(user):
437         bot.mumble.users[text.actor].send_text_message(
438             constants.strings('start_updating'))
439         msg = util.update(bot.version)
440         bot.mumble.users[text.actor].send_text_message(msg)
441     else:
442         bot.mumble.users[text.actor].send_text_message(
443             constants.strings('not_admin'))
444
445
446 def cmd_stop_and_getout(bot, user, text, command, parameter):
447     bot.stop()
448     if bot.channel:
449         bot.mumble.channels.find_by_name(bot.channel).move_in()
450
451
452 def cmd_volume(bot, user, text, command, parameter):
453     # The volume is a percentage
454     if parameter is not None and parameter.isdigit() and 0 <= int(parameter) <= 100:
455         bot.volume_set = float(float(parameter) / 100)
456         bot.send_msg(constants.strings('change_volume',
457             volume=int(bot.volume_set * 100), user=bot.mumble.users[text.actor]['name']), text)
458         var.db.set('bot', 'volume', str(bot.volume_set))
459         logging.info('cmd: volume set to %d' % (bot.volume_set * 100))
460     else:
461         bot.send_msg(constants.strings('current_volume', volume=int(bot.volume_set * 100)), text)
462
463
464 def cmd_ducking(bot, user, text, command, parameter):
465     if parameter == "" or parameter == "on":
466         bot.is_ducking = True
467         var.db.set('bot', 'ducking', True)
468         bot.ducking_volume = var.config.getfloat("bot", "ducking_volume", fallback=0.05)
469         bot.ducking_threshold = var.config.getint("bot", "ducking_threshold", fallback=5000)
470         bot.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_SOUNDRECEIVED,
471                                           bot.ducking_sound_received)
472         bot.mumble.set_receive_sound(True)
473         logging.info('cmd: ducking is on')
474         msg = "Ducking on."
475         bot.send_msg(msg, text)
476     elif parameter == "off":
477         bot.is_ducking = False
478         bot.mumble.set_receive_sound(False)
479         var.db.set('bot', 'ducking', False)
480         msg = "Ducking off."
481         logging.info('cmd: ducking is off')
482         bot.send_msg(msg, text)
483
484
485 def cmd_ducking_threshold(bot, user, text, command, parameter):
486     if parameter is not None and parameter.isdigit():
487         bot.ducking_threshold = int(parameter)
488         var.db.set('bot', 'ducking_threshold', str(bot.ducking_threshold))
489         msg = "Ducking threshold set to %d." % bot.ducking_threshold
490         bot.send_msg(msg, text)
491     else:
492         msg = "Current ducking threshold is %d." % bot.ducking_threshold
493         bot.send_msg(msg, text)
494
495
496 def cmd_ducking_volume(bot, user, text, command, parameter):
497     # The volume is a percentage
498     if parameter is not None and parameter.isdigit() and 0 <= int(parameter) <= 100:
499         bot.ducking_volume = float(float(parameter) / 100)
500         bot.send_msg(constants.strings('change_ducking_volume',
501             volume=int(bot.ducking_volume * 100), user=bot.mumble.users[text.actor]['name']), text)
502         # var.db.set('bot', 'volume', str(bot.volume_set))
503         var.db.set('bot', 'ducking_volume', str(bot.ducking_volume))
504         logging.info('cmd: volume on ducking set to %d' % (bot.ducking_volume * 100))
505     else:
506         bot.send_msg(constants.strings('current_ducking_volume', volume=int(bot.ducking_volume * 100)), text)
507
508
509 def cmd_current_music(bot, user, text, command, parameter):
510     reply = ""
511     if var.playlist.length() > 0:
512         bot.send_msg(util.format_current_playing())
513     else:
514         reply = constants.strings('not_playing')
515     bot.send_msg(reply, text)
516
517
518 def cmd_skip(bot, user, text, command, parameter):
519     if var.playlist.length() > 0:
520         bot.stop()
521         bot.launch_music()
522         bot.async_download_next()
523     else:
524         bot.send_msg(constants.strings('queue_empty'), text)
525
526
527 def cmd_remove(bot, user, text, command, parameter):
528     # Allow to remove specific music into the queue with a number
529     if parameter is not None and parameter.isdigit() and int(parameter) > 0 \
530             and int(parameter) <= var.playlist.length():
531
532         index = int(parameter) - 1
533
534         removed = None
535         if index == var.playlist.current_index:
536             removed = var.playlist.remove(index)
537             if bot.is_playing and not bot.is_pause:
538                 bot.stop()
539                 bot.launch_music(index)
540         else:
541             removed = var.playlist.remove(index)
542
543         # the Title isn't here if the music wasn't downloaded
544         bot.send_msg(constants.strings('removing_item',
545             item=removed['title'] if 'title' in removed else removed['url']), text)
546
547         logging.info("cmd: delete from playlist: " + str(removed['path'] if 'path' in removed else removed['url']))
548     else:
549         bot.send_msg(constants.strings('bad_parameter', command=command))
550
551
552 def cmd_list_file(bot, user, text, command, parameter):
553     folder_path = var.config.get('bot', 'music_folder')
554
555     files = util.get_recursive_filelist_sorted(folder_path)
556     msgs = [ "<br> <b>Files available:</b>" if not parameter else "<br> <b>Matched files:</b>" ]
557     try:
558         count = 0
559         for index, file in enumerate(files):
560             if parameter:
561                 match = re.search(parameter, file)
562                 if not match:
563                     continue
564
565             count += 1
566             msgs.append("<b>{:0>3d}</b> - {:s}".format(index, file))
567
568         if count != 0:
569             send_multi_lines(bot, msgs, text)
570         else:
571             bot.send_msg(constants.strings('no_file'), text)
572
573     except re.error as e:
574         msg = constants.strings('wrong_pattern', error=str(e))
575         bot.send_msg(msg, text)
576
577
578 def cmd_queue(bot, user, text, command, parameter):
579     if len(var.playlist.playlist) == 0:
580         msg = constants.strings('queue_empty')
581         bot.send_msg(msg, text)
582     else:
583         msgs = [ constants.strings('queue_contents')]
584         for i, value in enumerate(var.playlist.playlist):
585             newline = ''
586             if i == var.playlist.current_index:
587                 newline = '<b>{} ▶ ({}) {} ◀</b>'.format(i + 1, value['type'],
588                                                            value['title'] if 'title' in value else value['url'])
589             else:
590                 newline = '<b>{}</b> ({}) {}'.format(i + 1, value['type'],
591                                                      value['title'] if 'title' in value else value['url'])
592
593             msgs.append(newline)
594
595         send_multi_lines(bot, msgs, text)
596
597
598 def cmd_random(bot, user, text, command, parameter):
599     bot.stop()
600     var.playlist.randomize()
601     bot.launch_music(0)
602
603 def cmd_drop_database(bot, user, text, command, parameter):
604     var.db.drop_table()
605     var.db = Database(var.dbfile)
606     bot.send_msg(constants.strings('database_dropped'), text)