Example #1
0
 def validate_and_start_download(self, item):
     item.validate()
     if not item.is_ready():
         self.log.info("bot: current music isn't ready, start downloading.")
         self.async_download(item)
         self.send_channel_msg(
             constants.strings('download_in_progress',
                               item=item.format_title()))
 def check_update(self):
     self.log.debug("update: checking for updates...")
     new_version = util.new_release_version()
     if version.parse(new_version) > version.parse(self.version):
         changelog = util.fetch_changelog()
         self.log.info(
             "update: new version %s found, current installed version %s." %
             (new_version, self.version))
         self.log.info("update: changelog: " + changelog)
         changelog = changelog.replace("\n", "<br>")
         self.send_channel_msg(
             constants.strings('new_version_found',
                               new_version=new_version,
                               changelog=changelog))
     else:
         self.log.debug("update: no new version found.")
Example #3
0
 def check_update(self):
     self.log.debug("update: checking for updates...")
     new_version = util.new_release_version(
         var.config.get('bot', 'target_version'))
     if version.parse(new_version) > version.parse(
             self.version) and var.config.get('bot',
                                              'target_version') == "stable":
         changelog = util.fetch_changelog()
         self.log.info(
             f"update: new version {new_version} found, current installed version {self.version}."
         )
         self.log.info(f"update: changelog: {changelog}")
         changelog = changelog.replace("\n", "<br>")
         self.send_channel_msg(
             constants.strings('new_version_found',
                               new_version=new_version,
                               changelog=changelog))
     else:
         self.log.debug("update: no new version found.")
Example #4
0
    def users_changed(self, user, message):
        own_channel = self.mumble.channels[
            self.mumble.users.myself['channel_id']]
        # only check if there is one more user currently in the channel
        # else when the music is paused and somebody joins, music would start playing again
        if len(own_channel.get_users()) == 2:
            if var.config.get("bot",
                              "when_nobody_in_channel") == "pause_resume":
                self.resume()
            elif var.config.get("bot", "when_nobody_in_channel") == "pause":
                self.send_channel_msg(constants.strings("auto_paused"))

        elif len(own_channel.get_users()) == 1:
            # if the bot is the only user left in the channel
            self.log.info(
                'bot: Other users in the channel left. Stopping music now.')

            if var.config.get("bot", "when_nobody_in_channel") == "stop":
                self.clear()
            else:
                self.pause()
Example #5
0
    def loop(self):
        raw_music = ""
        while not self.exit and self.mumble.is_alive():

            while self.is_ffmpeg_running(
            ) and self.mumble.sound_output.get_buffer_size(
            ) > 0.5 and not self.exit:
                # If the buffer isn't empty, I cannot send new music part, so I wait
                self._loop_status = f'Wait for buffer {self.mumble.sound_output.get_buffer_size():.3f}'
                time.sleep(0.01)

            if self.is_ffmpeg_running():
                # I get raw from ffmpeg thread
                # move playhead forward
                self._loop_status = 'Reading raw'
                if self.song_start_at == -1:
                    self.song_start_at = time.time() - self.playhead
                self.playhead = time.time() - self.song_start_at

                raw_music = self.thread.stdout.read(self.pcm_buffer_size)
                self.read_pcm_size += self.pcm_buffer_size

                if self.redirect_ffmpeg_log:
                    try:
                        self.last_ffmpeg_err = self.thread_stderr.readline()
                        if self.last_ffmpeg_err:
                            self.log.debug("ffmpeg: " +
                                           self.last_ffmpeg_err.strip("\n"))
                    except:
                        pass

                if raw_music:
                    # Adjust the volume and send it to mumble
                    self.volume_cycle()

                    if not self.on_interrupting:
                        self.mumble.sound_output.add_sound(
                            audioop.mul(raw_music, 2,
                                        self.volume_helper.real_volume))
                    else:
                        self.mumble.sound_output.add_sound(
                            audioop.mul(self._fadeout(raw_music, self.stereo),
                                        2, self.volume_helper.real_volume))
                        self.thread.kill()
                        time.sleep(0.1)
                        self.on_interrupting = False
                else:
                    time.sleep(0.1)
            else:
                time.sleep(0.1)

            if not self.is_pause and not self.is_ffmpeg_running():
                # bot is not paused, but ffmpeg thread has gone.
                # indicate that last song has finished, or the bot just resumed from pause, or something is wrong.
                if self.read_pcm_size < self.pcm_buffer_size and len(var.playlist) > 0 \
                        and var.playlist.current_index != -1 \
                        and self.last_ffmpeg_err:
                    current = var.playlist.current_item()
                    self.log.error("bot: cannot play music %s",
                                   current.format_debug_string())
                    self.log.error("bot: with ffmpeg error: %s",
                                   self.last_ffmpeg_err)
                    self.last_ffmpeg_err = ""

                    self.send_channel_msg(
                        constants.strings('unable_play',
                                          item=current.format_title()))
                    var.playlist.remove_by_id(current.id)
                    var.cache.free_and_delete(current.id)

                # move to the next song.
                if not self.wait_for_ready:  # if wait_for_ready flag is not true, move to the next song.
                    if var.playlist.next():
                        current = var.playlist.current_item()
                        self.log.debug(
                            f"bot: next into the song: {current.format_debug_string()}"
                        )
                        try:
                            self.validate_and_start_download(current)
                            self.wait_for_ready = True

                            self.song_start_at = -1
                            self.playhead = 0

                        except ValidationFailedError as e:
                            self.send_channel_msg(e.msg)
                            var.playlist.remove_by_id(current.id)
                            var.cache.free_and_delete(current.id)
                    else:
                        self._loop_status = 'Empty queue'
                else:
                    # if wait_for_ready flag is true, means the pointer is already
                    # pointing to target song. start playing
                    current = var.playlist.current_item()
                    if current:
                        if current.is_ready():
                            self.wait_for_ready = False
                            self.read_pcm_size = 0

                            self.launch_music(current, self.playhead)
                            self.last_volume_cycle_time = time.time()
                            self.async_download_next()
                        elif current.is_failed():
                            var.playlist.remove_by_id(current.id)
                            self.wait_for_ready = False
                        else:
                            self._loop_status = 'Wait for the next item to be ready'
                    else:
                        self.wait_for_ready = False

        while self.mumble.sound_output.get_buffer_size() > 0:
            # Empty the buffer before exit
            time.sleep(0.01)
        time.sleep(0.5)

        if self.exit:
            self._loop_status = "exited"
            if var.config.getboolean('bot', 'save_playlist', fallback=True) \
                    and var.config.get("bot", "save_music_library", fallback=True):
                self.log.info("bot: save playlist into database")
                var.playlist.save()
Example #6
0
    def __init__(self, args):
        self.log = logging.getLogger("bot")
        self.log.info(f"bot: botamusique version {self.version}, starting...")
        signal.signal(signal.SIGINT, self.ctrl_caught)
        self.cmd_handle = {}

        self.stereo = var.config.getboolean('bot', 'stereo', fallback=True)

        if args.channel:
            self.channel = args.channel
        else:
            self.channel = var.config.get("server", "channel", fallback=None)

        var.user = args.user
        var.is_proxified = var.config.getboolean("webinterface",
                                                 "is_web_proxified")

        # Flags to indicate the bot is exiting (Ctrl-C, or !kill)
        self.exit = False
        self.nb_exit = 0

        # Related to ffmpeg thread
        self.thread = None
        self.thread_stderr = None
        self.read_pcm_size = 0
        self.pcm_buffer_size = 0
        self.last_ffmpeg_err = ""

        # Play/pause status
        self.is_pause = False
        self.pause_at_id = ""
        self.playhead = -1  # current position in a song.
        self.song_start_at = -1
        self.wait_for_ready = False  # flag for the loop are waiting for download to complete in the other thread

        #
        self.on_interrupting = False

        if args.host:
            host = args.host
        else:
            host = var.config.get("server", "host")

        if args.port:
            port = args.port
        else:
            port = var.config.getint("server", "port")

        if args.password:
            password = args.password
        else:
            password = var.config.get("server", "password")

        if args.channel:
            self.channel = args.channel
        else:
            self.channel = var.config.get("server", "channel")

        if args.certificate:
            certificate = args.certificate
        else:
            certificate = util.solve_filepath(
                var.config.get("server", "certificate"))

        if args.tokens:
            tokens = args.tokens
        else:
            tokens = var.config.get("server", "tokens")
            tokens = tokens.split(',')

        if args.user:
            self.username = args.user
        else:
            self.username = var.config.get("bot", "username")

        self.mumble = pymumble.Mumble(host,
                                      user=self.username,
                                      port=port,
                                      password=password,
                                      tokens=tokens,
                                      stereo=self.stereo,
                                      debug=var.config.getboolean(
                                          'debug', 'mumbleConnection'),
                                      certfile=certificate)
        self.mumble.callbacks.set_callback(
            pymumble.constants.PYMUMBLE_CLBK_TEXTMESSAGERECEIVED,
            self.message_received)

        self.mumble.set_codec_profile("audio")
        self.mumble.start()  # start the mumble thread
        self.mumble.is_ready()  # wait for the connection

        if self.mumble.connected >= pymumble_py3.constants.PYMUMBLE_CONN_STATE_FAILED:
            exit()

        self.set_comment()
        self.mumble.users.myself.unmute()  # by sure the user is not muted
        self.join_channel()
        self.mumble.set_bandwidth(200000)

        # ====== Volume ======
        self.volume_helper = util.VolumeHelper()

        _volume = var.config.getfloat('bot', 'volume', fallback=0.1)
        if var.db.has_option('bot', 'volume'):
            _volume = var.db.getfloat('bot', 'volume')
        self.volume_helper.set_volume(_volume)

        self.is_ducking = False
        self.on_ducking = False
        self.ducking_release = time.time()
        self.last_volume_cycle_time = time.time()

        self._ducking_volume = 0
        _ducking_volume = var.config.getfloat("bot",
                                              "ducking_volume",
                                              fallback=0.05)
        _ducking_volume = var.db.getfloat("bot",
                                          "ducking_volume",
                                          fallback=_ducking_volume)
        self.volume_helper.set_ducking_volume(_ducking_volume)

        self.ducking_threshold = var.config.getfloat("bot",
                                                     "ducking_threshold",
                                                     fallback=5000)
        self.ducking_threshold = var.db.getfloat(
            "bot", "ducking_threshold", fallback=self.ducking_threshold)

        if not var.db.has_option("bot", "ducking") and var.config.getboolean("bot", "ducking", fallback=False) \
                or var.config.getboolean("bot", "ducking"):
            self.is_ducking = True
            self.mumble.callbacks.set_callback(
                pymumble.constants.PYMUMBLE_CLBK_SOUNDRECEIVED,
                self.ducking_sound_received)
            self.mumble.set_receive_sound(True)

        assert var.config.get("bot", "when_nobody_in_channel") in ['pause', 'pause_resume', 'stop', 'nothing', ''], \
            "Unknown action for when_nobody_in_channel"

        if var.config.get("bot", "when_nobody_in_channel",
                          fallback='') in ['pause', 'pause_resume', 'stop']:
            user_change_callback = \
                lambda user, action: threading.Thread(target=self.users_changed, args=(user, action), daemon=True).start()
            self.mumble.callbacks.set_callback(
                pymumble.constants.PYMUMBLE_CLBK_USERREMOVED,
                user_change_callback)
            self.mumble.callbacks.set_callback(
                pymumble.constants.PYMUMBLE_CLBK_USERUPDATED,
                user_change_callback)

        # Debug use
        self._loop_status = 'Idle'
        self._display_rms = False
        self._max_rms = 0

        self.redirect_ffmpeg_log = var.config.getboolean('debug',
                                                         'redirect_ffmpeg_log',
                                                         fallback=True)

        if var.config.getboolean("bot", "auto_check_update"):
            th = threading.Thread(target=self.check_update,
                                  name="UpdateThread")
            th.daemon = True
            th.start()

        last_startup_version = var.db.get("bot", "version", fallback=None)
        if not last_startup_version or version.parse(
                last_startup_version) < version.parse(self.version):
            var.db.set("bot", "version", self.version)
            changelog = util.fetch_changelog().replace("\n", "<br>")
            self.send_channel_msg(
                constants.strings("update_successful",
                                  version=self.version,
                                  changelog=changelog))
Example #7
0
    def message_received(self, text):
        message = text.message.strip()
        user = self.mumble.users[text.actor]['name']

        if var.config.getboolean('commands', 'split_username_at_space'):
            # in can you use https://github.com/Natenom/mumblemoderator-module-collection/tree/master/os-suffixes ,
            # you want to split the username
            user = user.split()[0]

        if message[0] in var.config.get('commands', 'command_symbol'):
            # remove the symbol from the message
            message = message[1:].split(' ', 1)

            # use the first word as a command, the others one as  parameters
            if len(message) > 0:
                command = message[0].lower()
                parameter = ''
                if len(message) > 1:
                    parameter = message[1].rstrip()
            else:
                return

            self.log.info('bot: received command ' + command + ' - ' +
                          parameter + ' by ' + user)

            # Anti stupid guy function
            if not self.is_admin(user) and not var.config.getboolean(
                    'bot', 'allow_private_message') and text.session:
                self.mumble.users[text.actor].send_text_message(
                    constants.strings('pm_not_allowed'))
                return

            for i in var.db.items("user_ban"):
                if user.lower() == i[0]:
                    self.mumble.users[text.actor].send_text_message(
                        constants.strings('user_ban'))
                    return

            if not self.is_admin(user) and parameter:
                input_url = util.get_url_from_input(parameter)
                if input_url:
                    for i in var.db.items("url_ban"):
                        if input_url == i[0]:
                            self.mumble.users[text.actor].send_text_message(
                                constants.strings('url_ban'))
                            return

            command_exc = ""
            try:
                if command in self.cmd_handle:
                    command_exc = command
                else:
                    # try partial match
                    cmds = self.cmd_handle.keys()
                    matches = []
                    for cmd in cmds:
                        if cmd.startswith(command) and self.cmd_handle[cmd][
                                'partial_match']:
                            matches.append(cmd)

                    if len(matches) == 1:
                        self.log.info("bot: {:s} matches {:s}".format(
                            command, matches[0]))
                        command_exc = matches[0]

                    elif len(matches) > 1:
                        self.mumble.users[text.actor].send_text_message(
                            constants.strings('which_command',
                                              commands="<br>".join(matches)))
                        return
                    else:
                        self.mumble.users[text.actor].send_text_message(
                            constants.strings('bad_command', command=command))
                        return

                if self.cmd_handle[command_exc]['admin'] and not self.is_admin(
                        user):
                    self.mumble.users[text.actor].send_text_message(
                        constants.strings('not_admin'))
                    return

                if not self.cmd_handle[command_exc]['access_outside_channel'] \
                        and not self.is_admin(user) \
                        and not var.config.getboolean('bot', 'allow_other_channel_message') \
                        and self.mumble.users[text.actor]['channel_id'] != self.mumble.users.myself['channel_id']:
                    self.mumble.users[text.actor].send_text_message(
                        constants.strings('not_in_my_channel'))
                    return

                self.cmd_handle[command_exc]['handle'](self, user, text,
                                                       command_exc, parameter)
            except:
                error_traceback = traceback.format_exc()
                error = error_traceback.rstrip().split("\n")[-1]
                self.log.error(
                    f"bot: command {command_exc} failed with error: {error_traceback}\n"
                )
                self.send_msg(
                    constants.strings('error_executing_command',
                                      command=command_exc,
                                      error=error), text)