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