diff --git a/command.py b/command.py index cbdfd25..f22ab74 100644 --- a/command.py +++ b/command.py @@ -191,20 +191,29 @@ def cmd_url_unban(bot, user, text, command, parameter): def cmd_play(bot, user, text, command, parameter): global log - if len(var.playlist) > 0: - if parameter: - if parameter.isdigit() and 1 <= int(parameter) <= len(var.playlist): - # First "-1" transfer 12345 to 01234, second "-1" - # point to the previous item. the loop will next to - # the one you want - var.playlist.point_to(int(parameter) - 1 - 1) + params = parameter.split() + index = -1 + start_at = 0 + if len(params) > 0: + if params[0].isdigit() and 1 <= int(params[0]) <= len(var.playlist): + index = int(params[0]) + else: + bot.send_msg(constants.strings('invalid_index', index=parameter), text) - if not bot.is_pause: - bot.interrupt() + if len(params) > 1: + _start_at = params[1] + match = re.search("(?:(\d\d):)?(?:(\d\d):)?(\d\d(?:\.\d*)?)", _start_at, flags=re.IGNORECASE) + if match: + if match[1] is None and match[2] is None: + start_at = float(match[3]) + elif match[2] is None: + start_at = float(match[3]) + 60 * int(match[1]) else: - bot.is_pause = False - else: - bot.send_msg(constants.strings('invalid_index', index=parameter), text) + start_at = float(match[3]) + 60 * int(match[2]) + 3600 * int(match[2]) + + if len(var.playlist) > 0: + if index != -1: + bot.play(int(index) - 1, start_at) elif bot.is_pause: bot.resume() diff --git a/configuration.example.ini b/configuration.example.ini index 1a16509..3a3bd6c 100644 --- a/configuration.example.ini +++ b/configuration.example.ini @@ -108,7 +108,7 @@ port = 64738 # - "pause", # - "pause_resume" (pause and resume once somebody re-enters the channel) # - "stop" (also clears playlist) -# - "nothing" (do nothing) +# - "nothing" or leave empty (do nothing) #when_nobody_in_channel = nothing # [webinterface] stores settings related to the web interface. diff --git a/interface.py b/interface.py index a149fdc..e664000 100644 --- a/interface.py +++ b/interface.py @@ -328,11 +328,7 @@ def post(): log.info("web: jump to: " + music_wrapper.format_debug_string()) if len(var.playlist) >= int(request.form['play_music']): - var.playlist.point_to(int(request.form['play_music']) - 1) - if not var.bot.is_pause: - var.bot.interrupt() - else: - var.bot.is_pause = False + var.bot.play(int(request.form['play_music'])) time.sleep(0.1) elif 'delete_item_from_library' in request.form: diff --git a/media/playlist.py b/media/playlist.py index 4f11da1..e09c43a 100644 --- a/media/playlist.py +++ b/media/playlist.py @@ -249,6 +249,12 @@ class OneshotPlaylist(BasePlaylist): self.mode = "one-shot" self.current_index = -1 + def current_item(self): + if self.current_index == -1: + self.current_index = 0 + + return self[self.current_index] + def from_list(self, _list, current_index): if len(_list) > 0: if current_index > -1: @@ -259,6 +265,7 @@ class OneshotPlaylist(BasePlaylist): return self def next(self): + print(f"*** next asked") if len(self) > 0: self.version += 1 @@ -289,7 +296,7 @@ class OneshotPlaylist(BasePlaylist): def point_to(self, index): self.version += 1 self.current_index = -1 - for i in range(index + 1): + for i in range(index): super().__delitem__(0) diff --git a/mumbleBot.py b/mumbleBot.py index fc21e33..80bea46 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -136,6 +136,7 @@ class MumbleBot: self.is_ducking = False self.on_ducking = False self.ducking_release = time.time() + self.last_volume_cycle_time = time.time() if not var.db.has_option("bot", "ducking") and var.config.getboolean("bot", "ducking", fallback=False)\ or var.config.getboolean("bot", "ducking"): @@ -148,10 +149,10 @@ class MumbleBot: self.ducking_sound_received) self.mumble.set_receive_sound(True) - if var.config.get("bot", "when_nobody_in_channel") not in ['pause', 'pause_resume', 'stop', 'nothing']: - self.log.warn('Config "when_nobody_in_channel" is not on of "pause", "pause_resume", "stop" or "nothing", falling back to "nothing".') + 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='nothing') in ['pause', 'pause_resume', 'stop']: + if var.config.get("bot", "when_nobody_in_channel", fallback='') in ['pause', 'pause_resume', 'stop']: self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_USERREMOVED, self.users_changed) self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_USERUPDATED, self.users_changed) @@ -262,16 +263,6 @@ class MumbleBot: try: if command in self.cmd_handle: command_exc = command - - if not self.cmd_handle[command]['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]['handle'](self, user, text, command, parameter) else: # try partial match cmds = self.cmd_handle.keys() @@ -284,22 +275,24 @@ class MumbleBot: self.log.info("bot: {:s} matches {:s}".format(command, matches[0])) command_exc = matches[0] - 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) elif len(matches) > 1: self.mumble.users[text.actor].send_text_message( constants.strings('which_command', commands="
".join(matches))) + return else: self.mumble.users[text.actor].send_text_message( constants.strings('bad_command', command=command)) + 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] @@ -311,13 +304,13 @@ class MumbleBot: # text if the object message, contain information if direct message or channel message self.mumble.users[text.actor].send_text_message(msg) - def send_channel_msg(self, msg): msg = msg.encode('utf-8', 'ignore').decode('utf-8') own_channel = self.mumble.channels[self.mumble.users.myself['channel_id']] own_channel.send_text_message(msg) - def is_admin(self, user): + @staticmethod + def is_admin(user): list_admin = var.config.get('bot', 'admin').rstrip().split(';') if user in list_admin: return True @@ -338,7 +331,7 @@ class MumbleBot: 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: + 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.') @@ -351,12 +344,9 @@ class MumbleBot: # Launch and Download # ======================= - def launch_music(self): - if var.playlist.is_empty(): - return - assert self.wait_for_ready is False + def launch_music(self, music_wrapper, start_from=0): + assert music_wrapper.is_ready() - music_wrapper = var.playlist.current_item() uri = music_wrapper.uri() self.log.info("bot: play music " + music_wrapper.format_debug_string()) @@ -370,7 +360,7 @@ class MumbleBot: ffmpeg_debug = "warning" command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i', - uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-') + uri, '-ss', f"{start_from:f}", '-ac', '1', '-f', 's16le', '-ar', '48000', '-') self.log.debug("bot: execute ffmpeg command: " + " ".join(command)) # The ffmpeg process is a thread @@ -382,11 +372,6 @@ class MumbleBot: pipe_rd, pipe_wd = None, None self.thread = sp.Popen(command, stdout=sp.PIPE, stderr=pipe_wd, bufsize=480) - self.is_pause = False - self.read_pcm_size = 0 - self.song_start_at = -1 - self.playhead = 0 - self.last_volume_cycle_time = time.time() def async_download_next(self): # Function start if the next music isn't ready @@ -416,6 +401,14 @@ class MumbleBot: th.start() return th + 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 _download(self, item): ver = item.version try: @@ -471,7 +464,8 @@ class MumbleBot: time.sleep(0.1) if not self.is_pause and (self.thread is None or self.thread.poll() is not None): - # ffmpeg thread has gone. indicate that last song has finished, or something is wrong. + # 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 < 481 and len(var.playlist) > 0 and var.playlist.current_index != -1 \ and self.last_ffmpeg_err: current = var.playlist.current_item() @@ -488,26 +482,29 @@ class MumbleBot: if var.playlist.next(): current = var.playlist.current_item() try: - current.validate() - if not current.is_ready(): - self.log.info("bot: current music isn't ready, start downloading.") - self.async_download(current) - self.send_channel_msg( - constants.strings('download_in_progress', item=current.format_title())) + 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 + 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.launch_music() + 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) @@ -564,13 +561,25 @@ class MumbleBot: # Play Control # ======================= + def play(self, index=-1, start_at=0): + if not self.is_pause: + self.interrupt() + + if index != -1: + var.playlist.point_to(index) + + current = var.playlist.current_item() + + self.validate_and_start_download(current) + self.is_pause = False + self.wait_for_ready = True + self.song_start_at = -1 + self.playhead = start_at + def clear(self): # Kill the ffmpeg thread and empty the playlist - if self.thread: - self.thread.kill() - self.thread = None + self.interrupt() var.playlist.clear() - self.wait_for_ready = False self.log.info("bot: music stopped. playlist trashed.") def stop(self): @@ -586,6 +595,7 @@ class MumbleBot: self.thread.kill() self.thread = None self.song_start_at = -1 + self.read_pcm_size = 0 self.playhead = 0 def pause(self): @@ -599,44 +609,20 @@ class MumbleBot: self.log.info("bot: music paused at %.2f seconds." % self.playhead) def resume(self): + self.is_pause = False if var.playlist.current_index == -1: var.playlist.next() + self.playhead = 0 + return music_wrapper = var.playlist.current_item() if not music_wrapper or not music_wrapper.id == self.pause_at_id or not music_wrapper.is_ready(): - self.is_pause = False self.playhead = 0 return - if var.config.getboolean('debug', 'ffmpeg'): - ffmpeg_debug = "debug" - else: - ffmpeg_debug = "warning" - - self.log.info("bot: resume music at %.2f seconds" % self.playhead) - - uri = music_wrapper.uri() - - command = ("ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-ss', "%f" % self.playhead, '-i', - uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-') - - if var.config.getboolean('bot', 'announce_current_music'): - self.send_channel_msg(var.playlist.current_item().format_current_playing()) - - self.log.info("bot: execute ffmpeg command: " + " ".join(command)) - # The ffmpeg process is a thread - # prepare pipe for catching stderr of ffmpeg - if self.redirect_ffmpeg_log: - pipe_rd, pipe_wd = util.pipe_no_wait() # Let the pipe work in non-blocking mode - self.thread_stderr = os.fdopen(pipe_rd) - else: - pipe_rd, pipe_wd = None, None - - self.thread = sp.Popen(command, stdout=sp.PIPE, stderr=pipe_wd, bufsize=480) - self.last_volume_cycle_time = time.time() + self.wait_for_ready = True self.pause_at_id = "" - self.is_pause = False def start_web_interface(addr, port): @@ -735,7 +721,6 @@ if __name__ == '__main__': logging.getLogger("root").addHandler(handler) var.bot_logger = bot_logger - # ====================== # Load Database # ====================== diff --git a/static/css/custom.css b/static/css/custom.css index 8210e31..f9bbcf1 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -8,6 +8,9 @@ white-space: nowrap; overflow: hidden; } +.tag-space { + margin-right: 3px; +} .tag-click { cursor: pointer; transition: 400ms;