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