feat: three playback mode "one-shot", "loop", "random"

fix: bugs when you are on the last item and you want
remove it.

Some tips for testing:
Observe the behavior when you are playing the last
item and you remove the last item, for all three modes.
This commit is contained in:
Terry Geng 2020-02-26 22:09:53 +08:00
parent 388016a5af
commit 6a1320f8f9
5 changed files with 97 additions and 37 deletions

View File

@ -47,6 +47,7 @@ def register_all_commands(bot):
bot.register_command(constants.commands('list_file'), cmd_list_file)
bot.register_command(constants.commands('queue'), cmd_queue)
bot.register_command(constants.commands('random'), cmd_random)
bot.register_command(constants.commands('mode'), cmd_mode)
bot.register_command(constants.commands('drop_database'), cmd_drop_database)
def send_multi_lines(bot, lines, text):
@ -533,9 +534,17 @@ def cmd_remove(bot, user, text, command, parameter):
removed = None
if index == var.playlist.current_index:
removed = var.playlist.remove(index)
if bot.is_playing and not bot.is_pause:
bot.stop()
bot.launch_music(index)
if index < len(var.playlist):
if not bot.is_pause:
bot.kill_ffmpeg()
var.playlist.current_index -= 1
# then the bot will move to next item
else: # if item deleted is the last item of the queue
var.playlist.current_index -= 1
if not bot.is_pause:
bot.kill_ffmpeg()
else:
removed = var.playlist.remove(index)
@ -593,12 +602,25 @@ def cmd_queue(bot, user, text, command, parameter):
send_multi_lines(bot, msgs, text)
def cmd_random(bot, user, text, command, parameter):
bot.stop()
var.playlist.randomize()
bot.launch_music(0)
def cmd_mode(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings("current_mode", mode=var.playlist.mode), text)
return
if not parameter in ["one-shot", "loop", "random"]:
bot.send_msg(constants.strings('unknown_mode', mode=parameter), text)
else:
var.playlist.set_mode(parameter)
if parameter == "random":
bot.stop()
var.playlist.randomize()
bot.launch_music(0)
def cmd_drop_database(bot, user, text, command, parameter):
var.db.drop_table()
var.db = Database(var.dbfile)

View File

@ -38,6 +38,7 @@ admin = User1;User2;
music_folder = music_folder/
# Folder that stores the downloaded music.
tmp_folder = /tmp/
database_path = database.db
pip3_path = venv/bin/pip
auto_check_update = True
logfile =
@ -131,6 +132,7 @@ joinme = joinme
queue = queue
repeat = repeat
random = random
mode = mode
update = update
list_file = listfile
@ -183,6 +185,8 @@ database_dropped = Database dropped. All records have gone.
new_version_found = <h3>Update Available!</h3> New version of botamusique is available, send <i>!update</i> to update!
start_updating = Start updating...
file_missed = Music file '{file}' missed! This item has been removed from the playlist.
unknown_mode = Unknown playback mode '{mode}'. It should be one of <i>one-shot</i>, <i>loop</i>, <i>random</i>.
current_mode = Current playback mode is <i>{mode}</i>.
help = <h3>Commands</h3>
<b>Control</b>
@ -192,6 +196,8 @@ help = <h3>Commands</h3>
<li> <b>!<u>st</u>op </b> - stop playing </li>
<li> <b>!<u>sk</u>ip </b> - jump to the next song </li>
<li> <b>!<u>v</u>olume </b> {volume} - get or change the volume (from 0 to 100) </li>
<li> <b>!<u>m</u>ode </b> [{mode}] - get or set the playback mode, {mode} should be one of <i>one-shot</i> (play the playlist
once), <i>loop</i> (looping through the playlist), <i>random</i> (randomize the playlist)</li>
<li> <b>!duck </b> on/off - enable or disable ducking function </li>
<li> <b>!duckv </b> - set the volume of the bot when ducking is activated </li>
<li> <b>!<u>duckt</u>hres </b> - set the threshold of volume to activate ducking (3000 by default) </li>

View File

@ -220,13 +220,23 @@ def post():
logging.info("web: delete from playlist: " + util.format_debug_song_string(music))
if var.playlist.length() >= int(request.form['delete_music']):
if int(request.form['delete_music']) == var.playlist.current_index:
var.playlist.remove(int(request.form['delete_music']))
if var.botamusique.is_playing and not var.botamusique.is_pause:
var.botamusique.stop()
var.botamusique.launch_music(int(request.form['delete_music']))
index = int(request.form['delete_music'])
if index == var.playlist.current_index:
var.playlist.remove(index)
if index < len(var.playlist):
if not var.botamusique.is_pause:
var.botamusique.kill_ffmpeg()
var.playlist.current_index -= 1
# then the bot will move to next item
else: # if item deleted is the last item of the queue
var.playlist.current_index -= 1
if not var.botamusique.is_pause:
var.botamusique.kill_ffmpeg()
else:
var.playlist.remove(int(request.form['delete_music']))
var.playlist.remove(index)
elif 'play_music' in request.form:
@ -254,8 +264,12 @@ def post():
action = request.form['action']
if action == "randomize":
var.botamusique.stop()
var.playlist.randomize()
var.playlist.set_mode("random")
var.botamusique.resume()
if action == "one-shot":
var.playlist.set_mode("one-shot")
if action == "loop":
var.playlist.set_mode("loop")
elif action == "stop":
var.botamusique.stop()
elif action == "pause":
@ -269,12 +283,14 @@ def post():
var.botamusique.volume_set = var.botamusique.volume_set + 0.03
else:
var.botamusique.volume_set = 1.0
var.db.set('bot', 'volume', str(var.botamusique.volume_set))
logging.info("web: volume up to %d" % (var.botamusique.volume_set * 100))
elif action == "volume_down":
if var.botamusique.volume_set - 0.03 > 0:
var.botamusique.volume_set = var.botamusique.volume_set - 0.03
else:
var.botamusique.volume_set = 0
var.db.set('bot', 'volume', str(var.botamusique.volume_set))
logging.info("web: volume up to %d" % (var.botamusique.volume_set * 100))
if(var.playlist.length() > 0):

View File

@ -6,12 +6,20 @@ import json
import logging
class PlayList(list):
current_index = 0
current_index = -1
version = 0 # increase by one after each change
mode = "one-shot" # "loop", "random"
def __init__(self, *args):
super().__init__(*args)
def set_mode(self, mode):
# modes are "one-shot", "loop", "random"
self.mode = mode
var.db.set('playlist', 'mode', mode)
if mode == "random":
self.randomize()
def append(self, item):
self.version += 1
item = util.get_music_tag_info(item)
@ -45,15 +53,27 @@ class PlayList(list):
return items
def next(self):
self.version += 1
if len(self) == 0:
return False
self.version += 1
logging.debug("playlist: Next into the queue")
self.current_index = self.next_index()
return self[self.current_index]
if self.current_index < len(self) - 1:
self.current_index += 1
return self[self.current_index]
else:
self.current_index = 0
if self.mode == "one-shot":
self.clear()
return False
elif self.mode == "loop":
return self[0]
elif self.mode == "random":
self.randomize()
return self[0]
else:
raise TypeError("Unknown playlist mode '%s'." % self.mode)
def update(self, item, index=-1):
self.version += 1
@ -116,8 +136,8 @@ class PlayList(list):
def clear(self):
self.version += 1
self.current_index = 0
self.clear()
self.current_index = -1
super().clear()
def save(self):
var.db.remove_section("playlist_item")

View File

@ -90,8 +90,6 @@ class MumbleBot:
root.setLevel(logging.ERROR)
logging.error("Starting in ERROR loglevel")
var.playlist = PlayList()
var.user = args.user
var.music_folder = var.config.get('bot', 'music_folder')
var.is_proxified = var.config.getboolean(
@ -100,7 +98,6 @@ class MumbleBot:
self.nb_exit = 0
self.thread = None
self.thread_stderr = None
self.is_playing = False
self.is_pause = False
self.playhead = -1
self.song_start_at = -1
@ -378,7 +375,6 @@ class MumbleBot:
util.pipe_no_wait(pipe_rd) # Let the pipe work in non-blocking mode
self.thread_stderr = os.fdopen(pipe_rd)
self.thread = sp.Popen(command, stdout=sp.PIPE, stderr=pipe_wd, bufsize=480)
self.is_playing = True
self.is_pause = False
self.song_start_at = -1
self.playhead = 0
@ -560,11 +556,7 @@ class MumbleBot:
if self.thread is None or not raw_music:
# Not music into the buffet
if self.is_playing:
# get next music
self.is_playing = False
if not self.is_pause and len(var.playlist) > 0:
var.playlist.next()
if not self.is_pause and var.playlist.next():
self.launch_music()
self.async_download_next()
@ -610,35 +602,38 @@ class MumbleBot:
self.thread.kill()
self.thread = None
var.playlist.clear()
self.is_playing = False
logging.info("bot: music stopped. playlist trashed.")
def stop(self):
# stop and move to the next item in the playlist
self.is_pause = True
self.kill_ffmpeg()
self.playhead = 0
var.playlist.next()
logging.info("bot: music stopped.")
def kill_ffmpeg(self):
# Kill the ffmpeg thread
if self.thread:
self.thread.kill()
self.thread = None
self.is_playing = False
self.is_pause = True
self.song_start_at = -1
self.playhead = 0
var.playlist.next()
logging.info("bot: music stopped.")
def pause(self):
# Kill the ffmpeg thread
if self.thread:
self.thread.kill()
self.thread = None
self.is_playing = False
self.is_pause = True
self.song_start_at = -1
logging.info("bot: music paused at %.2f seconds." % self.playhead)
def resume(self):
self.is_playing = True
self.is_pause = False
if var.playlist.current_index == -1:
var.playlist.next()
music = var.playlist.current_item()
if music['type'] == 'radio' or self.playhead == 0 or not self.check_item_path_or_remove():
@ -700,7 +695,7 @@ if __name__ == '__main__':
parser.add_argument("--config", dest='config', type=str, default='configuration.ini',
help='Load configuration from this file. Default: configuration.ini')
parser.add_argument("--db", dest='db', type=str,
default='database.db', help='database file. Default: database.db')
default=None, help='database file. Default: database.db')
parser.add_argument("-q", "--quiet", dest="quiet",
action="store_true", help="Only Error logs")
@ -725,9 +720,9 @@ if __name__ == '__main__':
args = parser.parse_args()
var.dbfile = args.db
config = configparser.ConfigParser(interpolation=None, allow_no_value=True)
parsed_configs = config.read(['configuration.default.ini', args.config], encoding='utf-8')
var.dbfile = args.db if args.db is not None else config.get("bot", "database_path", fallback="database.db")
if len(parsed_configs) == 0:
logging.error('Could not read configuration from file \"{}\"'.format(
@ -753,6 +748,7 @@ if __name__ == '__main__':
handler.setFormatter(formatter)
root.addHandler(handler)
var.playlist = PlayList() # playlist should be initialized after the database
var.botamusique = MumbleBot(args)
command.register_all_commands(var.botamusique)