]> git.0d.be Git - botaradio.git/blob - command.py
fix: you can't just feed play_url wrong things
[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.system
9 import util
10 import variables as var
11 from librb import radiobrowser
12 from database import SettingsDatabase, MusicDatabase
13 from media.item import item_builders, item_loaders, item_id_generators, dict_to_item, dicts_to_items
14 from media.playlist import get_item_wrapper, get_item_wrapper_by_id, get_item_wrappers_by_tags
15 from media.file import FileItem
16 from media.url_from_playlist import PlaylistURLItem, get_playlist_info
17 from media.url import URLItem
18 from media.radio import RadioItem
19
20 log = logging.getLogger("bot")
21
22 def register_all_commands(bot):
23     bot.register_command(constants.commands('joinme'), cmd_joinme)
24     bot.register_command(constants.commands('user_ban'), cmd_user_ban)
25     bot.register_command(constants.commands('user_unban'), cmd_user_unban)
26     bot.register_command(constants.commands('url_ban'), cmd_url_ban)
27     bot.register_command(constants.commands('url_unban'), cmd_url_unban)
28     bot.register_command(constants.commands('play'), cmd_play)
29     bot.register_command(constants.commands('pause'), cmd_pause)
30     bot.register_command(constants.commands('play_file'), cmd_play_file)
31     bot.register_command(constants.commands('play_file_match'), cmd_play_file_match)
32     bot.register_command(constants.commands('play_url'), cmd_play_url)
33     bot.register_command(constants.commands('play_playlist'), cmd_play_playlist)
34     bot.register_command(constants.commands('play_radio'), cmd_play_radio)
35     bot.register_command(constants.commands('play_tag'), cmd_play_tags)
36     bot.register_command(constants.commands('rb_query'), cmd_rb_query)
37     bot.register_command(constants.commands('rb_play'), cmd_rb_play)
38     bot.register_command(constants.commands('yt_search'), cmd_yt_search)
39     bot.register_command(constants.commands('yt_play'), cmd_yt_play)
40     bot.register_command(constants.commands('help'), cmd_help)
41     bot.register_command(constants.commands('stop'), cmd_stop)
42     bot.register_command(constants.commands('clear'), cmd_clear)
43     bot.register_command(constants.commands('kill'), cmd_kill)
44     bot.register_command(constants.commands('update'), cmd_update)
45     bot.register_command(constants.commands('stop_and_getout'), cmd_stop_and_getout)
46     bot.register_command(constants.commands('volume'), cmd_volume)
47     bot.register_command(constants.commands('ducking'), cmd_ducking)
48     bot.register_command(constants.commands('ducking_threshold'), cmd_ducking_threshold)
49     bot.register_command(constants.commands('ducking_volume'), cmd_ducking_volume)
50     bot.register_command(constants.commands('current_music'), cmd_current_music)
51     bot.register_command(constants.commands('skip'), cmd_skip)
52     bot.register_command(constants.commands('last'), cmd_last)
53     bot.register_command(constants.commands('remove'), cmd_remove)
54     bot.register_command(constants.commands('list_file'), cmd_list_file)
55     bot.register_command(constants.commands('queue'), cmd_queue)
56     bot.register_command(constants.commands('random'), cmd_random)
57     bot.register_command(constants.commands('repeat'), cmd_repeat)
58     bot.register_command(constants.commands('mode'), cmd_mode)
59     bot.register_command(constants.commands('add_tag'), cmd_add_tag)
60     bot.register_command(constants.commands('remove_tag'), cmd_remove_tag)
61     bot.register_command(constants.commands('find_tagged'), cmd_find_tagged)
62     bot.register_command(constants.commands('search'), cmd_search_library)
63     bot.register_command(constants.commands('add_from_shortlist'), cmd_shortlist)
64     bot.register_command(constants.commands('drop_database'), cmd_drop_database, True)
65     bot.register_command(constants.commands('rescan'), cmd_refresh_cache, True)
66
67     # Just for debug use
68     bot.register_command('rtrms', cmd_real_time_rms, True)
69     bot.register_command('loop', cmd_loop_state, True)
70     bot.register_command('item', cmd_item, True)
71
72 def send_multi_lines(bot, lines, text, linebreak="<br />"):
73     global log
74
75     msg = ""
76     br = ""
77     for newline in lines:
78         msg += br
79         br = linebreak
80         if (len(msg) + len(newline)) > (bot.mumble.get_max_message_length() - 4) != 0: # 4 == len("<br>")
81             bot.send_msg(msg, text)
82             msg = ""
83         msg += newline
84
85     bot.send_msg(msg, text)
86
87 # ---------------- Variables -----------------
88
89 song_shortlist = []
90
91 # ---------------- Commands ------------------
92
93
94 def cmd_joinme(bot, user, text, command, parameter):
95     global log
96
97     bot.mumble.users.myself.move_in(
98         bot.mumble.users[text.actor]['channel_id'], token=parameter)
99
100
101 def cmd_user_ban(bot, user, text, command, parameter):
102     global log
103
104     if bot.is_admin(user):
105         if parameter:
106             bot.mumble.users[text.actor].send_text_message(util.user_ban(parameter))
107         else:
108             bot.mumble.users[text.actor].send_text_message(util.get_user_ban())
109     else:
110         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
111     return
112
113
114 def cmd_user_unban(bot, user, text, command, parameter):
115     global log
116
117     if bot.is_admin(user):
118         if parameter:
119             bot.mumble.users[text.actor].send_text_message(util.user_unban(parameter))
120     else:
121         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
122     return
123
124
125 def cmd_url_ban(bot, user, text, command, parameter):
126     global log
127
128     if bot.is_admin(user):
129         if parameter:
130             bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(parameter)))
131         else:
132             bot.mumble.users[text.actor].send_text_message(util.get_url_ban())
133     else:
134         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
135     return
136
137
138 def cmd_url_unban(bot, user, text, command, parameter):
139     global log
140
141     if bot.is_admin(user):
142         if parameter:
143             bot.mumble.users[text.actor].send_text_message(util.url_unban(util.get_url_from_input(parameter)))
144     else:
145         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
146     return
147
148
149 def cmd_play(bot, user, text, command, parameter):
150     global log
151
152     if len(var.playlist) > 0:
153         if parameter:
154             if parameter.isdigit() and 1 <= int(parameter) <= len(var.playlist):
155                 var.playlist.point_to(int(parameter) - 1 - 1) # First "-1" transfer 12345 to 01234, second "-1"
156                                                             # point to the previous item. the loop will next to
157                                                             # the one you want
158                 bot.interrupt()
159             else:
160                 bot.send_msg(constants.strings('invalid_index', index=parameter), text)
161
162         elif bot.is_pause:
163             bot.resume()
164         else:
165             bot.send_msg(var.playlist.current_item().format_current_playing(), text)
166     else:
167         bot.is_pause = False
168         bot.send_msg(constants.strings('queue_empty'), text)
169
170
171 def cmd_pause(bot, user, text, command, parameter):
172     global log
173
174     bot.pause()
175     bot.send_msg(constants.strings('paused'))
176
177
178 def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=False):
179     global log, song_shortlist
180
181     # if parameter is {index}
182     if parameter.isdigit():
183         files = var.cache.files
184         if int(parameter) < len(files):
185             music_wrapper = get_item_wrapper_by_id(bot, var.cache.file_id_lookup[files[int(parameter)]], user)
186             var.playlist.append(music_wrapper)
187             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
188             bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
189             return
190
191     # if parameter is {path}
192     else:
193         # sanitize "../" and so on
194         # path = os.path.abspath(os.path.join(var.music_folder, parameter))
195         # if not path.startswith(os.path.abspath(var.music_folder)):
196         #     bot.send_msg(constants.strings('no_file'), text)
197         #     return
198
199         if parameter in var.cache.files:
200             music_wrapper = get_item_wrapper(bot, type='file', path=parameter, user=user)
201             var.playlist.append(music_wrapper)
202             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
203             bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
204             return
205
206         # if parameter is {folder}
207         files = var.cache.dir.get_files(parameter)
208         if files:
209             msgs = [constants.strings('multiple_file_added')]
210             count = 0
211
212             for file in files:
213                 count += 1
214                 music_wrapper = get_item_wrapper_by_id(bot, var.cache.file_id_lookup[file], user)
215                 var.playlist.append(music_wrapper)
216                 log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
217                 msgs.append("{} ({})".format(music_wrapper.item().title, music_wrapper.item().path))
218
219             if count != 0:
220                 send_multi_lines(bot, msgs, text)
221                 return
222
223         else:
224             # try to do a partial match
225             files = var.cache.files
226             matches = [ file for file in files if parameter.lower() in file.lower()]
227             if len(matches) == 1:
228                 file = matches[0]
229                 music_wrapper = get_item_wrapper_by_id(bot, var.cache.file_id_lookup[file], user)
230                 var.playlist.append(music_wrapper)
231                 log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
232                 bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
233                 return
234             elif len(matches) > 1:
235                 msgs = [ constants.strings('multiple_matches') ]
236                 song_shortlist = []
237                 for index, match in enumerate(matches):
238                     id = var.cache.file_id_lookup[match]
239                     music_dict = var.music_db.query_music_by_id(id)
240                     item = dict_to_item(bot, music_dict)
241
242                     song_shortlist.append(music_dict)
243
244                     msgs.append("<b>{:d}</b> - <b>{:s}</b> ({:s})".format(
245                         index + 1, item.title, match))
246                 send_multi_lines(bot, msgs, text)
247                 return
248
249     if do_not_refresh_cache:
250         bot.send_msg(constants.strings("no_file"), text)
251     else:
252         var.cache.build_dir_cache(bot)
253         cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=True)
254
255
256 def cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cache=False):
257     global log
258
259     music_folder = var.music_folder
260     if parameter:
261         files = var.cache.files
262         msgs = [ constants.strings('multiple_file_added') + "<ul>"]
263         count = 0
264         try:
265             music_wrappers = []
266             for file in files:
267                 match = re.search(parameter, file)
268                 if match and match[0]:
269                     count += 1
270                     music_wrapper = get_item_wrapper_by_id(bot, var.cache.file_id_lookup[file], user)
271                     music_wrappers.append(music_wrapper)
272                     log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
273                     msgs.append("<li><b>{}</b> ({})</li>".format(music_wrapper.item().title,
274                                                  file[:match.span()[0]]
275                                                  + "<b style='color:pink'>"
276                                                  + file[match.span()[0]: match.span()[1]]
277                                                  + "</b>"
278                                                  + file[match.span()[1]:]
279                                                  ))
280
281             if count != 0:
282                 msgs.append("</ul>")
283                 var.playlist.extend(music_wrappers)
284                 send_multi_lines(bot, msgs, text, "")
285             else:
286                 if do_not_refresh_cache:
287                     bot.send_msg(constants.strings("no_file"), text)
288                 else:
289                     var.cache.build_dir_cache(bot)
290                     cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cache=True)
291
292         except re.error as e:
293             msg = constants.strings('wrong_pattern', error=str(e))
294             bot.send_msg(msg, text)
295     else:
296         bot.send_msg(constants.strings('bad_parameter', command=command))
297
298
299 def cmd_play_url(bot, user, text, command, parameter):
300     global log
301
302     url = util.get_url_from_input(parameter)
303     if url:
304         music_wrapper = get_item_wrapper(bot, type='url', url=url, user=user)
305         var.playlist.append(music_wrapper)
306
307         log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
308         bot.send_msg(constants.strings('file_added', item=music_wrapper.format_short_string()), text)
309         if len(var.playlist) == 2:
310             # If I am the second item on the playlist. (I am the next one!)
311             bot.async_download_next()
312     else:
313         bot.send_msg(constants.strings('bad_parameter', command=command))
314
315
316
317 def cmd_play_playlist(bot, user, text, command, parameter):
318     global log
319
320     offset = 0  # if you want to start the playlist at a specific index
321     try:
322         offset = int(parameter.split(" ")[-1])
323     except ValueError:
324         pass
325
326     url = util.get_url_from_input(parameter)
327     log.debug("cmd: fetching media info from playlist url %s" % url)
328     items = get_playlist_info(url=url, start_index=offset, user=user)
329     if len(items) > 0:
330         items = var.playlist.extend(list(map(
331             lambda item: get_item_wrapper(bot, **item), items)))
332         for music in items:
333             log.info("cmd: add to playlist: " + music.format_debug_string())
334     else:
335         bot.send_msg(constants.strings("playlist_fetching_failed"), text)
336
337
338 def cmd_play_radio(bot, user, text, command, parameter):
339     global log
340
341     if not parameter:
342         all_radio = var.config.items('radio')
343         msg = constants.strings('preconfigurated_radio')
344         for i in all_radio:
345             comment = ""
346             if len(i[1].split(maxsplit=1)) == 2:
347                 comment = " - " + i[1].split(maxsplit=1)[1]
348             msg += "<br />" + i[0] + comment
349         bot.send_msg(msg, text)
350     else:
351         if var.config.has_option('radio', parameter):
352             parameter = var.config.get('radio', parameter)
353             parameter = parameter.split()[0]
354         url = util.get_url_from_input(parameter)
355         if url:
356             music_wrapper = get_item_wrapper(bot, type='radio', url=url)
357
358             var.playlist.append(music_wrapper)
359             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
360         else:
361             bot.send_msg(constants.strings('bad_url'))
362
363
364 def cmd_rb_query(bot, user, text, command, parameter):
365     global log
366
367     log.info('cmd: Querying radio stations')
368     if not parameter:
369         log.debug('rbquery without parameter')
370         msg = constants.strings('rb_query_empty')
371         bot.send_msg(msg, text)
372     else:
373         log.debug('cmd: Found query parameter: ' + parameter)
374         # bot.send_msg('Searching for stations - this may take some seconds...', text)
375         rb_stations = radiobrowser.getstations_byname(parameter)
376         msg = constants.strings('rb_query_result')
377         msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th></tr>'
378         if not rb_stations:
379             log.debug('cmd: No matches found for rbquery ' + parameter)
380             bot.send_msg('Radio-Browser found no matches for ' + parameter, text)
381         else:
382             for s in rb_stations:
383                 stationid = s['id']
384                 stationname = s['stationname']
385                 country = s['country']
386                 codec = s['codec']
387                 bitrate = s['bitrate']
388                 genre = s['genre']
389                 # msg += f'<tr><td>{stationid}</td><td>{stationname}</td><td>{genre}</td><td>{codec}/{bitrate}</td><td>{country}</td></tr>'
390                 msg += '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s/%s</td><td>%s</td></tr>' % (
391                     stationid, stationname, genre, codec, bitrate, country)
392             msg += '</table>'
393             # Full message as html table
394             if len(msg) <= 5000:
395                 bot.send_msg(msg, text)
396             # Shorten message if message too long (stage I)
397             else:
398                 log.debug('Result too long stage I')
399                 msg = constants.strings('rb_query_result') + ' (shortened L1)'
400                 msg += '\n<table><tr><th>!rbplay ID</th><th>Station Name</th></tr>'
401                 for s in rb_stations:
402                     stationid = s['id']
403                     stationname = s['stationname']
404                     # msg += f'<tr><td>{stationid}</td><td>{stationname}</td>'
405                     msg += '<tr><td>%s</td><td>%s</td>' % (stationid, stationname)
406                 msg += '</table>'
407                 if len(msg) <= 5000:
408                     bot.send_msg(msg, text)
409                 # Shorten message if message too long (stage II)
410                 else:
411                     log.debug('Result too long stage II')
412                     msg = constants.strings('rb_query_result') + ' (shortened L2)'
413                     msg += '!rbplay ID - Station Name'
414                     for s in rb_stations:
415                         stationid = s['id']
416                         stationname = s['stationname'][:12]
417                         # msg += f'{stationid} - {stationname}'
418                         msg += '%s - %s' % (stationid, stationname)
419                     if len(msg) <= 5000:
420                         bot.send_msg(msg, text)
421                     # Message still too long
422                     else:
423                         bot.send_msg('Query result too long to post (> 5000 characters), please try another query.',
424                                      text)
425
426
427 def cmd_rb_play(bot, user, text, command, parameter):
428     global log
429
430     log.debug('cmd: Play a station by ID')
431     if not parameter:
432         log.debug('rbplay without parameter')
433         msg = constants.strings('rb_play_empty')
434         bot.send_msg(msg, text)
435     else:
436         log.debug('cmd: Retreiving url for station ID ' + parameter)
437         rstation = radiobrowser.getstationname_byid(parameter)
438         stationname = rstation[0]['name']
439         country = rstation[0]['country']
440         codec = rstation[0]['codec']
441         bitrate = rstation[0]['bitrate']
442         genre = rstation[0]['tags']
443         homepage = rstation[0]['homepage']
444         msg = 'Radio station added to playlist:'
445         # msg += '<table><tr><th>ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th><th>Homepage</th></tr>' + \
446         #       f'<tr><td>{parameter}</td><td>{stationname}</td><td>{genre}</td><td>{codec}/{bitrate}</td><td>{country}</td><td>{homepage}</td></tr></table>'
447         msg += '<table><tr><th>ID</th><th>Station Name</th><th>Genre</th><th>Codec/Bitrate</th><th>Country</th><th>Homepage</th></tr>' + \
448                '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s/%s</td><td>%s</td><td>%s</td></tr></table>' \
449                % (parameter, stationname, genre, codec, bitrate, country, homepage)
450         log.debug('cmd: Added station to playlist %s' % stationname)
451         bot.send_msg(msg, text)
452         url = radiobrowser.geturl_byid(parameter)
453         if url != "-1":
454             log.info('cmd: Found url: ' + url)
455             music_wrapper = get_item_wrapper(bot, type='radio', url=url, name=stationname, user=user)
456             var.playlist.append(music_wrapper)
457             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
458             bot.async_download_next()
459         else:
460             log.info('cmd: No playable url found.')
461             msg += "No playable url found for this station, please try another station."
462             bot.send_msg(msg, text)
463
464 yt_last_result = []
465 yt_last_page = 0 # TODO: if we keep adding global variables, we need to consider sealing all commands up into classes.
466
467 def cmd_yt_search(bot, user, text, command, parameter):
468     global log, yt_last_result, yt_last_page, song_shortlist
469     item_per_page = 5
470
471     if parameter:
472         # if next page
473         if parameter.startswith("-n"):
474             yt_last_page += 1
475             if len(yt_last_result) > yt_last_page * item_per_page:
476                 song_shortlist = [{'type': 'url',
477                                    'url': "https://www.youtube.com/watch?v=" + result[0],
478                                    'title': result[1]
479                                    } for result in yt_last_result[yt_last_page * item_per_page: item_per_page]]
480                 msg = _yt_format_result(yt_last_result, yt_last_page * item_per_page, item_per_page)
481                 bot.send_msg(constants.strings('yt_result', result_table=msg), text)
482             else:
483                 bot.send_msg(constants.strings('yt_no_more'))
484
485         # if query
486         else:
487             results = util.youtube_search(parameter)
488             if results:
489                 yt_last_result = results
490                 yt_last_page = 0
491                 song_shortlist = [{'type': 'url', 'url': "https://www.youtube.com/watch?v=" + result[0]}
492                                   for result in results[0: item_per_page]]
493                 msg = _yt_format_result(results, 0, item_per_page)
494                 bot.send_msg(constants.strings('yt_result', result_table=msg), text)
495             else:
496                 bot.send_msg(constants.strings('yt_query_error'))
497     else:
498         bot.send_msg(constants.strings('bad_parameter', command=command), text)
499
500 def _yt_format_result(results, start, count):
501     msg = '<table><tr><th width="10%">Index</th><th>Title</th><th width="20%">Uploader</th></tr>'
502     for index, item in enumerate(results[start:start+count]):
503         msg += '<tr><td>{index:d}</td><td>{title}</td><td>{uploader}</td></tr>'.format(
504             index=index + 1, title=item[1], uploader=item[2])
505     msg += '</table>'
506
507     return msg
508
509
510 def cmd_yt_play(bot, user, text, command, parameter):
511     global log, yt_last_result, yt_last_page
512
513     if parameter:
514         results = util.youtube_search(parameter)
515         if results:
516             yt_last_result = results
517             yt_last_page = 0
518             url = "https://www.youtube.com/watch?v=" + yt_last_result[0][0]
519             cmd_play_url(bot, user, text, command, url)
520         else:
521             bot.send_msg(constants.strings('yt_query_error'))
522     else:
523         bot.send_msg(constants.strings('bad_parameter', command=command), text)
524
525
526 def cmd_help(bot, user, text, command, parameter):
527     global log
528
529     bot.send_msg(constants.strings('help'), text)
530     if bot.is_admin(user):
531         bot.send_msg(constants.strings('admin_help'), text)
532
533
534 def cmd_stop(bot, user, text, command, parameter):
535     global log
536
537     bot.stop()
538     bot.send_msg(constants.strings('stopped'), text)
539
540
541 def cmd_clear(bot, user, text, command, parameter):
542     global log
543
544     bot.clear()
545     bot.send_msg(constants.strings('cleared'), text)
546
547
548 def cmd_kill(bot, user, text, command, parameter):
549     global log
550
551     if bot.is_admin(user):
552         bot.pause()
553         bot.exit = True
554     else:
555         bot.mumble.users[text.actor].send_text_message(
556             constants.strings('not_admin'))
557
558
559 def cmd_update(bot, user, text, command, parameter):
560     global log
561
562     if bot.is_admin(user):
563         bot.mumble.users[text.actor].send_text_message(
564             constants.strings('start_updating'))
565         msg = util.update(bot.version)
566         bot.mumble.users[text.actor].send_text_message(msg)
567     else:
568         bot.mumble.users[text.actor].send_text_message(
569             constants.strings('not_admin'))
570
571
572 def cmd_stop_and_getout(bot, user, text, command, parameter):
573     global log
574
575     bot.stop()
576     if bot.channel:
577         bot.mumble.channels.find_by_name(bot.channel).move_in()
578
579
580 def cmd_volume(bot, user, text, command, parameter):
581     global log
582
583     # The volume is a percentage
584     if parameter and parameter.isdigit() and 0 <= int(parameter) <= 100:
585         bot.volume_set = float(float(parameter) / 100)
586         bot.send_msg(constants.strings('change_volume',
587             volume=int(bot.volume_set * 100), user=bot.mumble.users[text.actor]['name']), text)
588         var.db.set('bot', 'volume', str(bot.volume_set))
589         log.info('cmd: volume set to %d' % (bot.volume_set * 100))
590     else:
591         bot.send_msg(constants.strings('current_volume', volume=int(bot.volume_set * 100)), text)
592
593
594 def cmd_ducking(bot, user, text, command, parameter):
595     global log
596
597     if parameter == "" or parameter == "on":
598         bot.is_ducking = True
599         var.db.set('bot', 'ducking', True)
600         bot.ducking_volume = var.config.getfloat("bot", "ducking_volume", fallback=0.05)
601         bot.ducking_threshold = var.config.getint("bot", "ducking_threshold", fallback=5000)
602         bot.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_SOUNDRECEIVED,
603                                           bot.ducking_sound_received)
604         bot.mumble.set_receive_sound(True)
605         log.info('cmd: ducking is on')
606         msg = "Ducking on."
607         bot.send_msg(msg, text)
608     elif parameter == "off":
609         bot.is_ducking = False
610         bot.mumble.set_receive_sound(False)
611         var.db.set('bot', 'ducking', False)
612         msg = "Ducking off."
613         log.info('cmd: ducking is off')
614         bot.send_msg(msg, text)
615
616
617 def cmd_ducking_threshold(bot, user, text, command, parameter):
618     global log
619
620     if parameter and parameter.isdigit():
621         bot.ducking_threshold = int(parameter)
622         var.db.set('bot', 'ducking_threshold', str(bot.ducking_threshold))
623         msg = "Ducking threshold set to %d." % bot.ducking_threshold
624         bot.send_msg(msg, text)
625     else:
626         msg = "Current ducking threshold is %d." % bot.ducking_threshold
627         bot.send_msg(msg, text)
628
629
630 def cmd_ducking_volume(bot, user, text, command, parameter):
631     global log
632
633     # The volume is a percentage
634     if parameter and parameter.isdigit() and 0 <= int(parameter) <= 100:
635         bot.ducking_volume = float(float(parameter) / 100)
636         bot.send_msg(constants.strings('change_ducking_volume',
637             volume=int(bot.ducking_volume * 100), user=bot.mumble.users[text.actor]['name']), text)
638         # var.db.set('bot', 'volume', str(bot.volume_set))
639         var.db.set('bot', 'ducking_volume', str(bot.ducking_volume))
640         log.info('cmd: volume on ducking set to %d' % (bot.ducking_volume * 100))
641     else:
642         bot.send_msg(constants.strings('current_ducking_volume', volume=int(bot.ducking_volume * 100)), text)
643
644
645 def cmd_current_music(bot, user, text, command, parameter):
646     global log
647
648     reply = ""
649     if len(var.playlist) > 0:
650         bot.send_msg(var.playlist.current_item().format_current_playing())
651     else:
652         reply = constants.strings('not_playing')
653     bot.send_msg(reply, text)
654
655
656 def cmd_skip(bot, user, text, command, parameter):
657     global log
658
659     bot.interrupt()
660
661     if len(var.playlist) == 0:
662         bot.send_msg(constants.strings('queue_empty'), text)
663
664
665 def cmd_last(bot, user, text, command, parameter):
666     global log
667
668     if len(var.playlist) > 0:
669         bot.interrupt()
670         var.playlist.point_to(len(var.playlist) - 1)
671     else:
672         bot.send_msg(constants.strings('queue_empty'), text)
673
674
675 def cmd_remove(bot, user, text, command, parameter):
676     global log
677
678     # Allow to remove specific music into the queue with a number
679     if parameter and parameter.isdigit() and int(parameter) > 0 \
680             and int(parameter) <= len(var.playlist):
681
682         index = int(parameter) - 1
683
684         removed = None
685         if index == var.playlist.current_index:
686             removed = var.playlist.remove(index)
687
688             if index < len(var.playlist):
689                 if not bot.is_pause:
690                     bot.interrupt()
691                     var.playlist.current_index -= 1
692                     # then the bot will move to next item
693
694             else: # if item deleted is the last item of the queue
695                 var.playlist.current_index -= 1
696                 if not bot.is_pause:
697                     bot.interrupt()
698         else:
699             removed = var.playlist.remove(index)
700
701         bot.send_msg(constants.strings('removing_item',
702             item=removed.format_short_string()), text)
703
704         log.info("cmd: delete from playlist: " + removed.format_debug_string())
705     else:
706         bot.send_msg(constants.strings('bad_parameter', command=command))
707
708
709 def cmd_list_file(bot, user, text, command, parameter):
710     global log
711
712     files = var.cache.files
713     msgs = [ "<br> <b>Files available:</b>" if not parameter else "<br> <b>Matched files:</b>" ]
714     try:
715         count = 0
716         for index, file in enumerate(files):
717             if parameter:
718                 match = re.search(parameter, file)
719                 if not match:
720                     continue
721
722             count += 1
723             msgs.append("<b>{:0>3d}</b> - {:s}".format(index, file))
724
725         if count != 0:
726             send_multi_lines(bot, msgs, text)
727         else:
728             bot.send_msg(constants.strings('no_file'), text)
729
730     except re.error as e:
731         msg = constants.strings('wrong_pattern', error=str(e))
732         bot.send_msg(msg, text)
733
734
735 def cmd_queue(bot, user, text, command, parameter):
736     global log
737
738     if len(var.playlist) == 0:
739         msg = constants.strings('queue_empty')
740         bot.send_msg(msg, text)
741     else:
742         msgs = [ constants.strings('queue_contents')]
743         for i, music in enumerate(var.playlist):
744             newline = ''
745             tags = ''
746             if len(music.item().tags) > 0:
747                 tags = "<sup>{}</sup>".format(", ".join(music.item().tags))
748             if i == var.playlist.current_index:
749                 newline = "<b style='color:orange'>{} ({}) {} </b> {}".format(i + 1, music.display_type(),
750                                                            music.format_short_string(), tags)
751             else:
752                 newline = '<b>{}</b> ({}) {} {}'.format(i + 1, music.display_type(),
753                                                            music.format_short_string(), tags)
754
755             msgs.append(newline)
756
757         send_multi_lines(bot, msgs, text)
758
759 def cmd_random(bot, user, text, command, parameter):
760     global log
761
762     bot.interrupt()
763     var.playlist.randomize()
764
765 def cmd_repeat(bot, user, text, command, parameter):
766     global log
767
768     repeat = 1
769     if parameter and parameter.isdigit():
770         repeat = int(parameter)
771
772     music = var.playlist.current_item()
773     for _ in range(repeat):
774         var.playlist.insert(
775             var.playlist.current_index + 1,
776             music
777         )
778         log.info("bot: add to playlist: " + music.format_debug_string)
779
780     bot.send_msg(constants.strings("repeat", song=music.format_song_string(), n=str(repeat)), text)
781
782 def cmd_mode(bot, user, text, command, parameter):
783     global log
784
785     if not parameter:
786         bot.send_msg(constants.strings("current_mode", mode=var.playlist.mode), text)
787         return
788     if not parameter in ["one-shot", "repeat", "random", "autoplay"]:
789         bot.send_msg(constants.strings('unknown_mode', mode=parameter), text)
790     else:
791         var.db.set('playlist', 'playback_mode', parameter)
792         var.playlist = media.playlist.get_playlist(parameter, var.playlist)
793         log.info("command: playback mode changed to %s." % parameter)
794         bot.send_msg(constants.strings("change_mode", mode=var.playlist.mode,
795                                        user=bot.mumble.users[text.actor]['name']), text)
796         if parameter == "random":
797             bot.interrupt()
798             bot.launch_music()
799
800 def cmd_play_tags(bot, user, text, command, parameter):
801     if not parameter:
802         bot.send_msg(constants.strings('bad_parameter', command=command))
803         return
804
805     msgs = [constants.strings('multiple_file_added') + "<ul>"]
806     count = 0
807
808     tags = parameter.split(",")
809     tags = list(map(lambda t: t.strip(), tags))
810     music_wrappers = get_item_wrappers_by_tags(bot, tags, user)
811     for music_wrapper in music_wrappers:
812         count += 1
813         log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
814         msgs.append("<li><b>{}</b> (<i>{}</i>)</li>".format(music_wrapper.item().title, ", ".join(music_wrapper.item().tags)))
815
816
817     if count != 0:
818         msgs.append("</ul>")
819         var.playlist.extend(music_wrappers)
820         send_multi_lines(bot, msgs, text, "")
821     else:
822         bot.send_msg(constants.strings("no_file"), text)
823
824
825 def cmd_add_tag(bot, user, text, command, parameter):
826     global log
827
828     params = parameter.split()
829     if len(params) == 2:
830         index = params[0]
831         tags = list(map(lambda t: t.strip(), params[1].split(",")))
832
833         if index.isdigit() and 1 <= int(index) <= len(var.playlist):
834             var.playlist[int(index) - 1].add_tags(tags)
835             log.info("cmd: add tags %s to song %s" % (", ".join(tags),
836                                                       var.playlist[int(index) - 1].format_debug_string()))
837             bot.send_msg(constants.strings("added_tags",
838                                            tags=", ".join(tags),
839                                            song=var.playlist[int(index) - 1].format_short_string()), text)
840         elif index == "*":
841             for item in var.playlist:
842                 item.add_tags(tags)
843                 log.info("cmd: add tags %s to song %s" % (", ".join(tags),
844                                                           item.format_debug_string()))
845             bot.send_msg(constants.strings("added_tags_to_all", tags=", ".join(tags)), text)
846         else:
847             bot.send_msg(constants.strings('bad_parameter', command=command), text)
848
849
850 def cmd_remove_tag(bot, user, text, command, parameter):
851     global log
852
853     params = parameter.split()
854     if len(params) == 2 and params[1]:
855         index = params[0]
856
857         if index.isdigit() and 1 <= int(index) <= len(var.playlist):
858             if params[1] != "*":
859                 tags = list(map(lambda t: t.strip(), params[1].split(",")))
860                 var.playlist[int(index) - 1].remove_tags(tags)
861                 log.info("cmd: remove tags %s from song %s" % (", ".join(tags),
862                                                           var.playlist[int(index) - 1].format_debug_string()))
863                 bot.send_msg(constants.strings("removed_tags",
864                                                tags=", ".join(tags),
865                                                song=var.playlist[int(index) - 1].format_short_string()), text)
866                 return
867             else:
868                 var.playlist[int(index) - 1].clear_tags()
869                 log.info("cmd: clear tags from song %s" % (var.playlist[int(index) - 1].format_debug_string()))
870                 bot.send_msg(constants.strings("cleared_tags",
871                                                song=var.playlist[int(index) - 1].format_short_string()), text)
872                 return
873
874         elif index == "*":
875             if params[1] != "*":
876                 tags = list(map(lambda t: t.strip(), params[1].split(",")))
877                 for item in var.playlist:
878                     item.remove_tags(tags)
879                     log.info("cmd: remove tags %s from song %s" % (", ".join(tags),
880                                                               item.format_debug_string()))
881                 bot.send_msg(constants.strings("removed_tags_from_all", tags=", ".join(tags)), text)
882                 return
883             else:
884                 for item in var.playlist:
885                     item.clear_tags()
886                     log.info("cmd: clear tags from song %s" % (item.format_debug_string()))
887                 bot.send_msg(constants.strings("cleared_tags_from_all"), text)
888                 return
889
890     bot.send_msg(constants.strings('bad_parameter', command=command), text)
891
892 def cmd_find_tagged(bot, user, text, command, parameter):
893     global song_shortlist
894
895     if not parameter:
896         bot.send_msg(constants.strings('bad_parameter', command=command))
897         return
898
899     msgs = [constants.strings('multiple_file_found') + "<ul>"]
900     count = 0
901
902     tags = parameter.split(",")
903     tags = list(map(lambda t: t.strip(), tags))
904
905     music_dicts = var.music_db.query_music_by_tags(tags)
906     song_shortlist = music_dicts
907     items = dicts_to_items(bot, music_dicts)
908
909     for i, item in enumerate(items):
910         count += 1
911         msgs.append("<li><b>{:d}</b> - <b>{}</b> (<i>{}</i>)</li>".format(i+1, item.title, ", ".join(item.tags)))
912
913     if count != 0:
914         msgs.append("</ul>")
915         msgs.append(constants.strings("shortlist_instruction"))
916         send_multi_lines(bot, msgs, text, "")
917     else:
918         bot.send_msg(constants.strings("no_file"), text)
919
920 def cmd_search_library(bot, user, text, command, parameter):
921     global song_shortlist
922     if not parameter:
923         bot.send_msg(constants.strings('bad_parameter', command=command))
924         return
925
926     msgs = [constants.strings('multiple_file_found') + "<ul>"]
927     count = 0
928
929     _keywords = parameter.split(" ")
930     keywords = []
931     for kw in _keywords:
932         if kw:
933             keywords.append(kw)
934
935     music_dicts = var.music_db.query_music_by_keywords(keywords)
936     items = dicts_to_items(bot, music_dicts)
937     song_shortlist = music_dicts
938
939     for item in items:
940         count += 1
941         if len(item.tags) > 0:
942             msgs.append("<li><b>{:d}</b> - [{}] <b>{}</b> (<i>{}</i>)</li>".format(count, item.display_type(), item.title, ", ".join(item.tags)))
943         else:
944             msgs.append("<li><b>{:d}</b> - [{}] <b>{}</b> </li>".format(count, item.display_type(), item.title, ", ".join(item.tags)))
945
946     if count != 0:
947         msgs.append("</ul>")
948         msgs.append(constants.strings("shortlist_instruction"))
949         send_multi_lines(bot, msgs, text, "")
950     else:
951         bot.send_msg(constants.strings("no_file"), text)
952
953
954 def cmd_shortlist(bot, user, text, command, parameter):
955     global song_shortlist
956     indexes = []
957     try:
958         indexes = [ int(i) for i in parameter.split(" ") ]
959     except ValueError:
960         bot.send_msg(constants.strings('bad_parameter', command=command), text)
961         return
962
963     if len(indexes) > 1:
964         msgs = [constants.strings('multiple_file_added') + "<ul>"]
965         for index in indexes:
966             if 1 <= index <= len(song_shortlist):
967                 kwargs = song_shortlist[index - 1]
968                 kwargs['user'] = user
969                 music_wrapper = get_item_wrapper(bot, **kwargs)
970                 var.playlist.append(music_wrapper)
971                 log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
972                 msgs.append("<li><b>{}</b></li>".format(music_wrapper.item().title))
973                 song_shortlist = []
974             else:
975                 bot.send_msg(constants.strings('bad_parameter', command=command), text)
976                 return
977
978             msgs.append("</ul>")
979             send_multi_lines(bot, msgs, text, "")
980             song_shortlist = []
981             return
982     elif len(indexes) == 1:
983         index = indexes[0]
984         if 1 <= index <= len(song_shortlist):
985             kwargs = song_shortlist[index - 1]
986             kwargs['user'] = user
987             music_wrapper = get_item_wrapper(bot, **kwargs)
988             var.playlist.append(music_wrapper)
989             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
990             bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
991             song_shortlist = []
992             return
993
994     bot.send_msg(constants.strings('bad_parameter', command=command), text)
995
996
997 def cmd_drop_database(bot, user, text, command, parameter):
998     global log
999
1000     if bot.is_admin(user):
1001         var.db.drop_table()
1002         var.db = SettingsDatabase(var.dbfile)
1003         var.music_db.drop_table()
1004         var.music_db = MusicDatabase(var.dbfile)
1005         log.info("command: database dropped.")
1006         bot.send_msg(constants.strings('database_dropped'), text)
1007     else:
1008         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
1009
1010 def cmd_refresh_cache(bot, user, text, command, parameter):
1011     global log
1012     if bot.is_admin(user):
1013         var.cache.build_dir_cache(bot)
1014         log.info("command: Local file cache refreshed.")
1015         bot.send_msg(constants.strings('cache_refreshed'), text)
1016     else:
1017         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
1018
1019 # Just for debug use
1020 def cmd_real_time_rms(bot, user, text, command, parameter):
1021     bot._display_rms = not bot._display_rms
1022
1023 def cmd_loop_state(bot, user, text, command, parameter):
1024     print(bot._loop_status)
1025
1026 def cmd_item(bot, user, text, command, parameter):
1027     print(bot.wait_for_downloading)
1028     print(var.playlist.current_item().to_dict())