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('list_file'), cmd_list_file)
bot.register_command(constants.commands('queue'), cmd_queue) bot.register_command(constants.commands('queue'), cmd_queue)
bot.register_command(constants.commands('random'), cmd_random) 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) bot.register_command(constants.commands('drop_database'), cmd_drop_database)
def send_multi_lines(bot, lines, text): def send_multi_lines(bot, lines, text):
@ -533,9 +534,17 @@ def cmd_remove(bot, user, text, command, parameter):
removed = None removed = None
if index == var.playlist.current_index: if index == var.playlist.current_index:
removed = var.playlist.remove(index) removed = var.playlist.remove(index)
if bot.is_playing and not bot.is_pause:
bot.stop() if index < len(var.playlist):
bot.launch_music(index) 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: else:
removed = var.playlist.remove(index) removed = var.playlist.remove(index)
@ -593,12 +602,25 @@ def cmd_queue(bot, user, text, command, parameter):
send_multi_lines(bot, msgs, text) send_multi_lines(bot, msgs, text)
def cmd_random(bot, user, text, command, parameter): def cmd_random(bot, user, text, command, parameter):
bot.stop() bot.stop()
var.playlist.randomize() var.playlist.randomize()
bot.launch_music(0) 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): def cmd_drop_database(bot, user, text, command, parameter):
var.db.drop_table() var.db.drop_table()
var.db = Database(var.dbfile) var.db = Database(var.dbfile)

View File

@ -38,6 +38,7 @@ admin = User1;User2;
music_folder = music_folder/ music_folder = music_folder/
# Folder that stores the downloaded music. # Folder that stores the downloaded music.
tmp_folder = /tmp/ tmp_folder = /tmp/
database_path = database.db
pip3_path = venv/bin/pip pip3_path = venv/bin/pip
auto_check_update = True auto_check_update = True
logfile = logfile =
@ -131,6 +132,7 @@ joinme = joinme
queue = queue queue = queue
repeat = repeat repeat = repeat
random = random random = random
mode = mode
update = update update = update
list_file = listfile 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! new_version_found = <h3>Update Available!</h3> New version of botamusique is available, send <i>!update</i> to update!
start_updating = Start updating... start_updating = Start updating...
file_missed = Music file '{file}' missed! This item has been removed from the playlist. 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> help = <h3>Commands</h3>
<b>Control</b> <b>Control</b>
@ -192,6 +196,8 @@ help = <h3>Commands</h3>
<li> <b>!<u>st</u>op </b> - stop playing </li> <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>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>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>!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>!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> <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)) logging.info("web: delete from playlist: " + util.format_debug_song_string(music))
if var.playlist.length() >= int(request.form['delete_music']): if var.playlist.length() >= int(request.form['delete_music']):
if int(request.form['delete_music']) == var.playlist.current_index: index = int(request.form['delete_music'])
var.playlist.remove(int(request.form['delete_music']))
if var.botamusique.is_playing and not var.botamusique.is_pause: if index == var.playlist.current_index:
var.botamusique.stop() var.playlist.remove(index)
var.botamusique.launch_music(int(request.form['delete_music']))
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: else:
var.playlist.remove(int(request.form['delete_music'])) var.playlist.remove(index)
elif 'play_music' in request.form: elif 'play_music' in request.form:
@ -254,8 +264,12 @@ def post():
action = request.form['action'] action = request.form['action']
if action == "randomize": if action == "randomize":
var.botamusique.stop() var.botamusique.stop()
var.playlist.randomize() var.playlist.set_mode("random")
var.botamusique.resume() var.botamusique.resume()
if action == "one-shot":
var.playlist.set_mode("one-shot")
if action == "loop":
var.playlist.set_mode("loop")
elif action == "stop": elif action == "stop":
var.botamusique.stop() var.botamusique.stop()
elif action == "pause": elif action == "pause":
@ -269,12 +283,14 @@ def post():
var.botamusique.volume_set = var.botamusique.volume_set + 0.03 var.botamusique.volume_set = var.botamusique.volume_set + 0.03
else: else:
var.botamusique.volume_set = 1.0 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)) logging.info("web: volume up to %d" % (var.botamusique.volume_set * 100))
elif action == "volume_down": elif action == "volume_down":
if var.botamusique.volume_set - 0.03 > 0: if var.botamusique.volume_set - 0.03 > 0:
var.botamusique.volume_set = var.botamusique.volume_set - 0.03 var.botamusique.volume_set = var.botamusique.volume_set - 0.03
else: else:
var.botamusique.volume_set = 0 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)) logging.info("web: volume up to %d" % (var.botamusique.volume_set * 100))
if(var.playlist.length() > 0): if(var.playlist.length() > 0):

View File

@ -6,12 +6,20 @@ import json
import logging import logging
class PlayList(list): class PlayList(list):
current_index = 0 current_index = -1
version = 0 # increase by one after each change version = 0 # increase by one after each change
mode = "one-shot" # "loop", "random"
def __init__(self, *args): def __init__(self, *args):
super().__init__(*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): def append(self, item):
self.version += 1 self.version += 1
item = util.get_music_tag_info(item) item = util.get_music_tag_info(item)
@ -45,15 +53,27 @@ class PlayList(list):
return items return items
def next(self): def next(self):
self.version += 1
if len(self) == 0: if len(self) == 0:
return False return False
self.version += 1
logging.debug("playlist: Next into the queue") logging.debug("playlist: Next into the queue")
self.current_index = self.next_index() if self.current_index < len(self) - 1:
self.current_index += 1
return self[self.current_index] 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): def update(self, item, index=-1):
self.version += 1 self.version += 1
@ -116,8 +136,8 @@ class PlayList(list):
def clear(self): def clear(self):
self.version += 1 self.version += 1
self.current_index = 0 self.current_index = -1
self.clear() super().clear()
def save(self): def save(self):
var.db.remove_section("playlist_item") var.db.remove_section("playlist_item")

View File

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