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