]> git.0d.be Git - botaradio.git/blob - mumbleBot.py
MERGED develop to master
[botaradio.git] / mumbleBot.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3
4 import threading
5 import time
6 import sys
7 import math
8 import signal
9 import configparser
10 import audioop
11 import subprocess as sp
12 import argparse
13 import os
14 import os.path
15 import pymumble.pymumble_py3 as pymumble
16 import variables as var
17 import logging
18 import logging.handlers
19 import traceback
20 from packaging import version
21
22 import util
23 import command
24 import constants
25 from database import SettingsDatabase, MusicDatabase
26 import media.system
27 from media.playlist import BasePlaylist
28 from media.cache import MusicCache
29
30
31 class MumbleBot:
32     version = '5.2'
33
34     def __init__(self, args):
35         self.log = logging.getLogger("bot")
36         self.log.info("bot: botamusique version %s, starting..." % self.version)
37         signal.signal(signal.SIGINT, self.ctrl_caught)
38         self.cmd_handle = {}
39         self.volume_set = var.config.getfloat('bot', 'volume')
40         if var.db.has_option('bot', 'volume'):
41             self.volume_set = var.db.getfloat('bot', 'volume')
42
43         self.volume = self.volume_set
44
45         if args.channel:
46             self.channel = args.channel
47         else:
48             self.channel = var.config.get("server", "channel", fallback=None)
49
50         if args.verbose:
51             self.log.setLevel(logging.DEBUG)
52             self.log.debug("Starting in DEBUG loglevel")
53         elif args.quiet:
54             self.log.setLevel(logging.ERROR)
55             self.log.error("Starting in ERROR loglevel")
56
57         var.user = args.user
58         var.music_folder = util.solve_filepath(var.config.get('bot', 'music_folder'))
59         var.tmp_folder = util.solve_filepath(var.config.get('bot', 'tmp_folder'))
60         var.is_proxified = var.config.getboolean(
61             "webinterface", "is_web_proxified")
62         self.exit = False
63         self.nb_exit = 0
64         self.thread = None
65         self.thread_stderr = None
66         self.is_pause = False
67         self.pause_at_id = ""
68         self.playhead = -1
69         self.song_start_at = -1
70         self.last_ffmpeg_err = ""
71         self.read_pcm_size = 0
72         # self.download_threads = []
73         self.wait_for_downloading = False  # flag for the loop are waiting for download to complete in the other thread
74
75         if var.config.getboolean("webinterface", "enabled"):
76             wi_addr = var.config.get("webinterface", "listening_addr")
77             wi_port = var.config.getint("webinterface", "listening_port")
78             tt = threading.Thread(
79                 target=start_web_interface, name="WebThread", args=(wi_addr, wi_port))
80             tt.daemon = True
81             self.log.info('Starting web interface on {}:{}'.format(wi_addr, wi_port))
82             tt.start()
83
84         if var.config.getboolean("bot", "auto_check_update"):
85             th = threading.Thread(target=self.check_update, name="UpdateThread")
86             th.daemon = True
87             th.start()
88
89         if args.host:
90             host = args.host
91         else:
92             host = var.config.get("server", "host")
93
94         if args.port:
95             port = args.port
96         else:
97             port = var.config.getint("server", "port")
98
99         if args.password:
100             password = args.password
101         else:
102             password = var.config.get("server", "password")
103
104         if args.channel:
105             self.channel = args.channel
106         else:
107             self.channel = var.config.get("server", "channel")
108
109         if args.certificate:
110             certificate = args.certificate
111         else:
112             certificate = util.solve_filepath(var.config.get("server", "certificate"))
113
114         if args.tokens:
115             tokens = args.tokens
116         else:
117             tokens = var.config.get("server", "tokens")
118             tokens = tokens.split(',')
119
120         if args.user:
121             self.username = args.user
122         else:
123             self.username = var.config.get("bot", "username")
124
125         self.mumble = pymumble.Mumble(host, user=self.username, port=port, password=password, tokens=tokens,
126                                       debug=var.config.getboolean('debug', 'mumbleConnection'), certfile=certificate)
127         self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_TEXTMESSAGERECEIVED, self.message_received)
128
129         self.mumble.set_codec_profile("audio")
130         self.mumble.start()  # start the mumble thread
131         self.mumble.is_ready()  # wait for the connection
132         self.set_comment()
133         self.mumble.users.myself.unmute()  # by sure the user is not muted
134         if self.channel:
135             self.mumble.channels.find_by_name(self.channel).move_in()
136         self.mumble.set_bandwidth(200000)
137
138         self.is_ducking = False
139         self.on_ducking = False
140         self.ducking_release = time.time()
141
142         if not var.db.has_option("bot", "ducking") and var.config.getboolean("bot", "ducking", fallback=False)\
143                 or var.config.getboolean("bot", "ducking"):
144             self.is_ducking = True
145             self.ducking_volume = var.config.getfloat("bot", "ducking_volume", fallback=0.05)
146             self.ducking_volume = var.db.getfloat("bot", "ducking_volume", fallback=self.ducking_volume)
147             self.ducking_threshold = var.config.getfloat("bot", "ducking_threshold", fallback=5000)
148             self.ducking_threshold = var.db.getfloat("bot", "ducking_threshold", fallback=self.ducking_threshold)
149             self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_SOUNDRECEIVED,
150                                                self.ducking_sound_received)
151             self.mumble.set_receive_sound(True)
152
153         # Debug use
154         self._loop_status = 'Idle'
155         self._display_rms = False
156         self._max_rms = 0
157
158     # Set the CTRL+C shortcut
159     def ctrl_caught(self, signal, frame):
160
161         self.log.info(
162             "\nSIGINT caught, quitting, {} more to kill".format(2 - self.nb_exit))
163         self.exit = True
164         self.pause()
165         if self.nb_exit > 1:
166             self.log.info("Forced Quit")
167             sys.exit(0)
168         self.nb_exit += 1
169
170         if var.config.getboolean('bot', 'save_playlist', fallback=True) \
171                 and var.config.get("bot", "save_music_library", fallback=True):
172             self.log.info("bot: save playlist into database")
173             var.playlist.save()
174
175     def check_update(self):
176         self.log.debug("update: checking for updates...")
177         new_version = util.new_release_version()
178         if version.parse(new_version) > version.parse(self.version):
179             self.log.info("update: new version %s found, current installed version %s." % (new_version, self.version))
180             self.send_msg(constants.strings('new_version_found'))
181         else:
182             self.log.debug("update: no new version found.")
183
184     def register_command(self, cmd, handle, no_partial_match=False, access_outside_channel=False):
185         cmds = cmd.split(",")
186         for command in cmds:
187             command = command.strip()
188             if command:
189                 self.cmd_handle[command] = {'handle': handle,
190                                             'partial_match': not no_partial_match,
191                                             'access_outside_channel': access_outside_channel}
192                 self.log.debug("bot: command added: " + command)
193
194     def set_comment(self):
195         self.mumble.users.myself.comment(var.config.get('bot', 'comment'))
196
197     # =======================
198     #         Message
199     # =======================
200
201     # All text send to the chat is analysed by this function
202     def message_received(self, text):
203         message = text.message.strip()
204         user = self.mumble.users[text.actor]['name']
205
206         if var.config.getboolean('commands', 'split_username_at_space'):
207             # in can you use https://github.com/Natenom/mumblemoderator-module-collection/tree/master/os-suffixes ,
208             # you want to split the username
209             user = user.split()[0]
210
211         if message[0] in var.config.get('commands', 'command_symbol'):
212             # remove the symbol from the message
213             message = message[1:].split(' ', 1)
214
215             # use the first word as a command, the others one as  parameters
216             if len(message) > 0:
217                 command = message[0].lower()
218                 parameter = ''
219                 if len(message) > 1:
220                     parameter = message[1].rstrip()
221             else:
222                 return
223
224             self.log.info('bot: received command ' + command + ' - ' + parameter + ' by ' + user)
225
226             # Anti stupid guy function
227             if not self.is_admin(user) and not var.config.getboolean('bot', 'allow_private_message') and text.session:
228                 self.mumble.users[text.actor].send_text_message(
229                     constants.strings('pm_not_allowed'))
230                 return
231
232             for i in var.db.items("user_ban"):
233                 if user.lower() == i[0]:
234                     self.mumble.users[text.actor].send_text_message(
235                         constants.strings('user_ban'))
236                     return
237
238             if not self.is_admin(user) and parameter:
239                 input_url = util.get_url_from_input(parameter)
240                 if input_url:
241                     for i in var.db.items("url_ban"):
242                         if input_url == i[0]:
243                             self.mumble.users[text.actor].send_text_message(
244                                 constants.strings('url_ban'))
245                             return
246
247             command_exc = ""
248             try:
249                 if command in self.cmd_handle:
250                     command_exc = command
251
252                     if not self.cmd_handle[command]['access_outside_channel'] \
253                             and not self.is_admin(user) \
254                             and not var.config.getboolean('bot', 'allow_other_channel_message') \
255                             and self.mumble.users[text.actor]['channel_id'] != self.mumble.users.myself['channel_id']:
256                         self.mumble.users[text.actor].send_text_message(
257                             constants.strings('not_in_my_channel'))
258                         return
259
260                     self.cmd_handle[command]['handle'](self, user, text, command, parameter)
261                 else:
262                     # try partial match
263                     cmds = self.cmd_handle.keys()
264                     matches = []
265                     for cmd in cmds:
266                         if cmd.startswith(command) and self.cmd_handle[cmd]['partial_match']:
267                             matches.append(cmd)
268
269                     if len(matches) == 1:
270                         self.log.info("bot: {:s} matches {:s}".format(command, matches[0]))
271                         command_exc = matches[0]
272
273                         if not self.cmd_handle[command_exc]['access_outside_channel'] \
274                                 and not self.is_admin(user) \
275                                 and not var.config.getboolean('bot', 'allow_other_channel_message') \
276                                 and self.mumble.users[text.actor]['channel_id'] != self.mumble.users.myself[
277                                 'channel_id']:
278                             self.mumble.users[text.actor].send_text_message(
279                                 constants.strings('not_in_my_channel'))
280                             return
281
282                         self.cmd_handle[command_exc]['handle'](self, user, text, command_exc, parameter)
283                     elif len(matches) > 1:
284                         self.mumble.users[text.actor].send_text_message(
285                             constants.strings('which_command', commands="<br>".join(matches)))
286                     else:
287                         self.mumble.users[text.actor].send_text_message(
288                             constants.strings('bad_command', command=command))
289             except:
290                 error_traceback = traceback.format_exc()
291                 error = error_traceback.rstrip().split("\n")[-1]
292                 self.log.error("bot: command %s failed with error: %s\n" % (command_exc, error_traceback))
293                 self.send_msg(constants.strings('error_executing_command', command=command_exc, error=error), text)
294
295     def send_msg(self, msg, text=None):
296         msg = msg.encode('utf-8', 'ignore').decode('utf-8')
297         # text if the object message, contain information if direct message or channel message
298         if not text:
299             own_channel = self.mumble.channels[self.mumble.users.myself['channel_id']]
300             own_channel.send_text_message(msg)
301         else:
302             self.mumble.users[text.actor].send_text_message(msg)
303
304     def is_admin(self, user):
305         list_admin = var.config.get('bot', 'admin').rstrip().split(';')
306         if user in list_admin:
307             return True
308         else:
309             return False
310
311     # =======================
312     #   Launch and Download
313     # =======================
314
315     def launch_music(self):
316         if var.playlist.is_empty():
317             return
318         assert self.wait_for_downloading is False
319
320         music_wrapper = var.playlist.current_item()
321         uri = music_wrapper.uri()
322
323         self.log.info("bot: play music " + music_wrapper.format_debug_string())
324
325         if var.config.getboolean('bot', 'announce_current_music'):
326             self.send_msg(music_wrapper.format_current_playing())
327
328         if var.config.getboolean('debug', 'ffmpeg'):
329             ffmpeg_debug = "debug"
330         else:
331             ffmpeg_debug = "warning"
332
333         command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i',
334                    uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-')
335         self.log.debug("bot: execute ffmpeg command: " + " ".join(command))
336
337         # The ffmpeg process is a thread
338         # prepare pipe for catching stderr of ffmpeg
339         pipe_rd, pipe_wd = os.pipe()
340         util.pipe_no_wait(pipe_rd)  # Let the pipe work in non-blocking mode
341         self.thread_stderr = os.fdopen(pipe_rd)
342         self.thread = sp.Popen(command, stdout=sp.PIPE, stderr=pipe_wd, bufsize=480)
343         self.is_pause = False
344         self.read_pcm_size = 0
345         self.song_start_at = -1
346         self.playhead = 0
347         self.last_volume_cycle_time = time.time()
348
349     def async_download_next(self):
350         # Function start if the next music isn't ready
351         # Do nothing in case the next music is already downloaded
352         self.log.debug("bot: Async download next asked ")
353         while var.playlist.next_item() and var.playlist.next_item().type in ['url', 'url_from_playlist']:
354             # usually, all validation will be done when adding to the list.
355             # however, for performance consideration, youtube playlist won't be validate when added.
356             # the validation has to be done here.
357             next = var.playlist.next_item()
358             if next.validate():
359                 if not next.is_ready():
360                     var.playlist.async_prepare(var.playlist.next_index())
361                 break
362             else:
363                 var.playlist.remove_by_id(next.id)
364                 var.cache.free_and_delete(next.id)
365
366     # =======================
367     #          Loop
368     # =======================
369
370     # Main loop of the Bot
371     def loop(self):
372         raw_music = ""
373         while not self.exit and self.mumble.is_alive():
374
375             while self.thread and self.mumble.sound_output.get_buffer_size() > 0.5 and not self.exit:
376                 # If the buffer isn't empty, I cannot send new music part, so I wait
377                 self._loop_status = 'Wait for buffer %.3f' % self.mumble.sound_output.get_buffer_size()
378                 time.sleep(0.01)
379
380             if self.thread:
381                 # I get raw from ffmpeg thread
382                 # move playhead forward
383                 self._loop_status = 'Reading raw'
384                 if self.song_start_at == -1:
385                     self.song_start_at = time.time() - self.playhead
386                 self.playhead = time.time() - self.song_start_at
387
388                 raw_music = self.thread.stdout.read(480)
389                 self.read_pcm_size += 480
390
391                 try:
392                     self.last_ffmpeg_err = self.thread_stderr.readline()
393                     if self.last_ffmpeg_err:
394                         self.log.debug("ffmpeg: " + self.last_ffmpeg_err.strip("\n"))
395                 except:
396                     pass
397
398                 if raw_music:
399                     # Adjust the volume and send it to mumble
400                     self.volume_cycle()
401                     self.mumble.sound_output.add_sound(
402                         audioop.mul(raw_music, 2, self.volume))
403                 else:
404                     time.sleep(0.1)
405             else:
406                 time.sleep(0.1)
407
408             if not self.is_pause and (self.thread is None or not raw_music):
409                 # ffmpeg thread has gone. indicate that last song has finished, or something is wrong.
410                 if self.read_pcm_size < 481 and len(var.playlist) > 0 and var.playlist.current_index != -1 \
411                         and self.last_ffmpeg_err:
412                     current = var.playlist.current_item()
413                     self.log.error("bot: cannot play music %s", current.format_debug_string())
414                     self.log.error("bot: with ffmpeg error: %s", self.last_ffmpeg_err)
415                     self.last_ffmpeg_err = ""
416
417                     self.send_msg(constants.strings('unable_play', item=current.format_short_string()))
418                     var.playlist.remove_by_id(current.id)
419                     var.cache.free_and_delete(current.id)
420
421                 # move to the next song.
422                 if not self.wait_for_downloading:
423                     if var.playlist.next():
424                         current = var.playlist.current_item()
425                         if current.validate():
426                             if current.is_ready():
427                                 self.launch_music()
428                                 self.async_download_next()
429                             else:
430                                 self.log.info("bot: current music isn't ready, start downloading.")
431                                 self.wait_for_downloading = True
432                                 var.playlist.async_prepare(var.playlist.current_index)
433                                 self.send_msg(constants.strings('download_in_progress', item=current.format_short_string()))
434                         else:
435                             var.playlist.remove_by_id(current.id)
436                             var.cache.free_and_delete(current.id)
437                     else:
438                         self._loop_status = 'Empty queue'
439                 else:
440                     current = var.playlist.current_item()
441                     if current:
442                         if current.is_ready():
443                             self.wait_for_downloading = False
444                             var.playlist.version += 1
445                             self.launch_music()
446                             self.async_download_next()
447                         elif current.is_failed():
448                             var.playlist.remove_by_id(current.id)
449                         else:
450                             self._loop_status = 'Wait for downloading'
451                     else:
452                         self.wait_for_downloading = False
453
454         while self.mumble.sound_output.get_buffer_size() > 0:
455             # Empty the buffer before exit
456             time.sleep(0.01)
457         time.sleep(0.5)
458
459         if self.exit:
460             self._loop_status = "exited"
461             if var.config.getboolean('bot', 'save_playlist', fallback=True) \
462                     and var.config.get("bot", "save_music_library", fallback=True):
463                 self.log.info("bot: save playlist into database")
464                 var.playlist.save()
465
466     def volume_cycle(self):
467         delta = time.time() - self.last_volume_cycle_time
468
469         if self.on_ducking and self.ducking_release < time.time():
470             self.on_ducking = False
471             self._max_rms = 0
472
473         if delta > 0.001:
474             if self.is_ducking and self.on_ducking:
475                 self.volume = (self.volume - self.ducking_volume) * math.exp(- delta / 0.2) + self.ducking_volume
476             else:
477                 self.volume = self.volume_set - (self.volume_set - self.volume) * math.exp(- delta / 0.5)
478
479         self.last_volume_cycle_time = time.time()
480
481     def ducking_sound_received(self, user, sound):
482         rms = audioop.rms(sound.pcm, 2)
483         self._max_rms = max(rms, self._max_rms)
484         if self._display_rms:
485             if rms < self.ducking_threshold:
486                 print('%6d/%6d  ' % (rms, self._max_rms) + '-'*int(rms/200), end='\r')
487             else:
488                 print('%6d/%6d  ' % (rms, self._max_rms) + '-'*int(self.ducking_threshold/200)
489                       + '+'*int((rms - self.ducking_threshold)/200), end='\r')
490
491         if rms > self.ducking_threshold:
492             if self.on_ducking is False:
493                 self.log.debug("bot: ducking triggered")
494                 self.on_ducking = True
495             self.ducking_release = time.time() + 1  # ducking release after 1s
496
497     # =======================
498     #      Play Control
499     # =======================
500
501     def clear(self):
502         # Kill the ffmpeg thread and empty the playlist
503         if self.thread:
504             self.thread.kill()
505             self.thread = None
506         var.playlist.clear()
507         self.log.info("bot: music stopped. playlist trashed.")
508
509     def stop(self):
510         self.interrupt()
511         self.is_pause = True
512         self.log.info("bot: music stopped.")
513
514     def interrupt(self):
515         # Kill the ffmpeg thread
516         if self.thread:
517             self.thread.kill()
518             self.thread = None
519         self.song_start_at = -1
520         self.playhead = 0
521
522     def pause(self):
523         # Kill the ffmpeg thread
524         if self.thread:
525             self.pause_at_id = var.playlist.current_item().id
526             self.thread.kill()
527             self.thread = None
528         self.is_pause = True
529         self.song_start_at = -1
530         self.log.info("bot: music paused at %.2f seconds." % self.playhead)
531
532     def resume(self):
533         self.is_pause = False
534
535         if var.playlist.current_index == -1:
536             var.playlist.next()
537
538         music_wrapper = var.playlist.current_item()
539
540         if not music_wrapper or not music_wrapper.id == self.pause_at_id or not music_wrapper.is_ready():
541             self.playhead = 0
542             return
543
544         if var.config.getboolean('debug', 'ffmpeg'):
545             ffmpeg_debug = "debug"
546         else:
547             ffmpeg_debug = "warning"
548
549         self.log.info("bot: resume music at %.2f seconds" % self.playhead)
550
551         uri = music_wrapper.uri()
552
553         command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-ss', "%f" % self.playhead, '-i',
554                    uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-')
555
556         if var.config.getboolean('bot', 'announce_current_music'):
557             self.send_msg(var.playlist.current_item().format_current_playing())
558
559         self.log.info("bot: execute ffmpeg command: " + " ".join(command))
560         # The ffmpeg process is a thread
561         # prepare pipe for catching stderr of ffmpeg
562         pipe_rd, pipe_wd = os.pipe()
563         util.pipe_no_wait(pipe_rd)  # Let the pipe work in non-blocking mode
564         self.thread_stderr = os.fdopen(pipe_rd)
565         self.thread = sp.Popen(command, stdout=sp.PIPE, stderr=pipe_wd, bufsize=480)
566         self.last_volume_cycle_time = time.time()
567         self.pause_at_id = ""
568
569
570 def start_web_interface(addr, port):
571     global formatter
572     import interface
573
574     # setup logger
575     werkzeug_logger = logging.getLogger('werkzeug')
576     logfile = util.solve_filepath(var.config.get('webinterface', 'web_logfile'))
577     if logfile:
578         handler = logging.handlers.RotatingFileHandler(logfile, mode='a', maxBytes=10240)  # Rotate after 10KB
579     else:
580         handler = logging.StreamHandler()
581
582     werkzeug_logger.addHandler(handler)
583
584     interface.init_proxy()
585     interface.web.env = 'development'
586     interface.web.run(port=port, host=addr)
587
588
589 if __name__ == '__main__':
590     parser = argparse.ArgumentParser(
591         description='Bot for playing music on Mumble')
592
593     # General arguments
594     parser.add_argument("--config", dest='config', type=str, default='configuration.ini',
595                         help='Load configuration from this file. Default: configuration.ini')
596     parser.add_argument("--db", dest='db', type=str,
597                         default=None, help='database file. Default: database.db')
598
599     parser.add_argument("-q", "--quiet", dest="quiet",
600                         action="store_true", help="Only Error logs")
601     parser.add_argument("-v", "--verbose", dest="verbose",
602                         action="store_true", help="Show debug log")
603
604     # Mumble arguments
605     parser.add_argument("-s", "--server", dest="host",
606                         type=str, help="Hostname of the Mumble server")
607     parser.add_argument("-u", "--user", dest="user",
608                         type=str, help="Username for the bot")
609     parser.add_argument("-P", "--password", dest="password",
610                         type=str, help="Server password, if required")
611     parser.add_argument("-T", "--tokens", dest="tokens",
612                         type=str, help="Server tokens, if required")
613     parser.add_argument("-p", "--port", dest="port",
614                         type=int, help="Port for the Mumble server")
615     parser.add_argument("-c", "--channel", dest="channel",
616                         type=str, help="Default channel for the bot")
617     parser.add_argument("-C", "--cert", dest="certificate",
618                         type=str, default=None, help="Certificate file")
619
620     args = parser.parse_args()
621
622     config = configparser.ConfigParser(interpolation=None, allow_no_value=True)
623     parsed_configs = config.read([util.solve_filepath('configuration.default.ini'), util.solve_filepath(args.config)],
624                                  encoding='utf-8')
625     var.dbfile = args.db if args.db is not None else util.solve_filepath(
626         config.get("bot", "database_path", fallback="database.db"))
627
628     if len(parsed_configs) == 0:
629         logging.error('Could not read configuration from file \"{}\"'.format(args.config))
630         sys.exit()
631
632     var.config = config
633     var.db = SettingsDatabase(var.dbfile)
634
635     # Setup logger
636     bot_logger = logging.getLogger("bot")
637     formatter = logging.Formatter('[%(asctime)s %(levelname)s %(threadName)s] %(message)s', "%b %d %H:%M:%S")
638     bot_logger.setLevel(logging.INFO)
639
640     logfile = util.solve_filepath(var.config.get('bot', 'logfile'))
641     handler = None
642     if logfile:
643         handler = logging.handlers.RotatingFileHandler(logfile, mode='a', maxBytes=10240)  # Rotate after 10KB
644     else:
645         handler = logging.StreamHandler()
646
647     handler.setFormatter(formatter)
648     bot_logger.addHandler(handler)
649     var.bot_logger = bot_logger
650
651     if var.config.get("bot", "save_music_library", fallback=True):
652         var.music_db = MusicDatabase(var.dbfile)
653     else:
654         var.music_db = MusicDatabase(":memory:")
655
656     var.cache = MusicCache(var.music_db)
657
658     # load playback mode
659     playback_mode = None
660     if var.db.has_option("playlist", "playback_mode"):
661         playback_mode = var.db.get('playlist', 'playback_mode')
662     else:
663         playback_mode = var.config.get('bot', 'playback_mode', fallback="one-shot")
664
665     if playback_mode in ["one-shot", "repeat", "random", "autoplay"]:
666         var.playlist = media.playlist.get_playlist(playback_mode)
667     else:
668         raise KeyError("Unknown playback mode '%s'" % playback_mode)
669
670     var.bot = MumbleBot(args)
671     command.register_all_commands(var.bot)
672
673     if var.config.get("bot", "refresh_cache_on_startup", fallback=True)\
674             or not var.db.has_option("dir_cache", "files"):
675         var.cache.build_dir_cache(var.bot)
676     else:
677         var.cache.load_dir_cache(var.bot)
678
679     # load playlist
680     if var.config.getboolean('bot', 'save_playlist', fallback=True):
681         var.bot_logger.info("bot: load playlist from previous session")
682         var.playlist.load()
683
684     # Start the main loop.
685     var.bot.loop()