diff --git a/README.md b/README.md
index 59e6d9f..62c6592 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,7 @@ username = Bragi
music_folder = music_folder/
tmp_folder = /tmp/
volume = 0.5
+restore_volume_on_stop = False
```
**Generate Certificate (Recommended):**
diff --git a/bragi.py b/bragi.py
index f944296..3552134 100755
--- a/bragi.py
+++ b/bragi.py
@@ -716,6 +716,7 @@ class MumbleBot:
var.cache.free_and_delete(current.id)
else:
self._loop_status = 'Empty queue'
+ self.restore_default_volume()
else:
# if wait_for_ready flag is true, means the pointer is already
# pointing to target song. start playing
@@ -855,6 +856,7 @@ class MumbleBot:
self._cleanup_ffmpeg_process() # Ensure proper cleanup
var.playlist.clear()
self.wait_for_ready = False
+ self.restore_default_volume()
self.log.info("bot: music stopped. playlist trashed.")
def stop(self):
@@ -865,8 +867,21 @@ class MumbleBot:
self.wait_for_ready = True
else:
self.wait_for_ready = False
+ self.restore_default_volume()
self.log.info("bot: music stopped.")
+ def restore_default_volume(self):
+ if not var.config.getboolean("bot", "restore_volume_on_stop"):
+ return
+
+ max_volume = var.config.getfloat("bot", "max_volume")
+ if var.db.has_option("bot", "max_volume"):
+ max_volume = var.db.getfloat("bot", "max_volume")
+
+ target_volume = min(var.config.getfloat("bot", "volume"), max_volume)
+ self.volume_helper.set_volume(target_volume)
+ var.db.set("bot", "volume", str(target_volume))
+
def _cleanup_ffmpeg_process(self):
"""Properly cleanup FFmpeg process and associated resources"""
if self.thread:
diff --git a/configuration.default.ini b/configuration.default.ini
index cdfd4e8..b873dd6 100644
--- a/configuration.default.ini
+++ b/configuration.default.ini
@@ -61,6 +61,7 @@ playback_mode = one-shot
rebuild_workers = 0
redirect_stderr = True
refresh_cache_on_startup = True
+restore_volume_on_stop = False
save_music_library = True
save_playlist = True
stereo = True
@@ -155,4 +156,3 @@ yt_search = ysearch
#
# File paths use fuzzy matching - the first matching file in the music database will be used
# URLs are downloaded and played like the !url command
-
diff --git a/configuration.example.ini b/configuration.example.ini
index 318ce4a..7767cb8 100644
--- a/configuration.example.ini
+++ b/configuration.example.ini
@@ -53,7 +53,10 @@ port = 64738
# 'volume': The default volume, a number from 0 to 1.
# This option will be overridden by the value set in the database.
+# 'restore_volume_on_stop': When True, stopping playback restores the live and
+# persisted volume back to this config-file default volume.
#volume = 0.1
+#restore_volume_on_stop = False
# 'bandwidth': The number of bits per second used by the bot when streaming audio.
# Enabling this option will allow you to set it higher than the default value.
diff --git a/constants.py b/constants.py
index 05b04d9..06a426b 100644
--- a/constants.py
+++ b/constants.py
@@ -30,19 +30,6 @@ def tr_cli(option, *argv, **kwargs):
except KeyError:
raise KeyError("Missed strings in language file: '{string}'. ".format(string=option))
return _tr(string, *argv, **kwargs)
-
-
-def tr_web(option, *argv, **kwargs):
- try:
- if option in lang_dict['web'] and lang_dict['web'][option]:
- string = lang_dict['web'][option]
- else:
- string = default_lang_dict['web'][option]
- except KeyError:
- raise KeyError("Missed strings in language file: '{string}'. ".format(string=option))
- return _tr(string, *argv, **kwargs)
-
-
def _tr(string, *argv, **kwargs):
if argv or kwargs:
try:
diff --git a/lang/en_US.json b/lang/en_US.json
index 0b96ed9..041af8c 100644
--- a/lang/en_US.json
+++ b/lang/en_US.json
@@ -2,7 +2,7 @@
"cli": {
"added_tags": "Added tags {tags} to {song}.",
"added_tags_to_all": "Added tags {tags} to songs on the playlist.",
- "admin_help": "
Admin command
\nBot\n\n- !kill - kill the bot
\n- !update - update the bot
\n- !userban {user} - ban a user
\n- !userunban {user} - unban a user
\n- !urlbanlist - list banned url
\n- !urlban [{url}] - ban {url} (or current item's url by default) and remove this url from the library.
\n- !urlunban {url} - unban {url}
\n- !rescan {url} - rebuild local music file cache
\n- !dropdatabase - clear the entire database, you will lose all settings and music library.
\n
\nWeb Interface\n\n- !webuserlist - list all users that have the permission of accessing the web interface, if auth mode is 'password'.
\n- !webuseradd {nick name} - grant the user with {nick name} the access to the web interface, if auth mode is 'password'.
\n- !webuserdel {nick name} - revoke the access to the web interface of {nick name}, if auth mode is 'password'.
\n
",
+ "admin_help": "Admin command
\nBot\n\n- !kill - kill the bot
\n- !update - update the bot
\n- !userban {user} - ban a user
\n- !userunban {user} - unban a user
\n- !urlbanlist - list banned url
\n- !urlban [{url}] - ban {url} (or current item's url by default) and remove this url from the library.
\n- !urlunban {url} - unban {url}
\n- !rescan {url} - rebuild local music file cache
\n- !dropdatabase - clear the entire database, you will lose all settings and music library.
\n- !maxvolume {volume} - set the maximum allowed volume.
\n
",
"auto_paused": "Use !play to resume music!",
"bad_command": "{command}: command not found.",
"bad_parameter": "{command}: invalid parameter.",
@@ -28,7 +28,7 @@
"file_deleted": "Deleted {item} from the library.",
"file_item": "{artist} - {title} added by {user}",
"file_missed": "Music file '{file}' missed! This item has been removed from the playlist.",
- "help": "Commands
\nControl\n\n- !web - get the URL of the web interface, if enabled.
\n- !play (or !p) [{num}] [{start_from}] - resume from pausing / start to play (the num-th song is num if given)
\n- !pause - pause
\n- !stop - stop playing
\n- !skip - jump to the next song
\n- !last - jump to the last song
\n- !volume {volume} - get or change the volume (from 0 to 100)
\n- !mode [{mode}] - get or set the playback mode, {mode} should be one of one-shot (remove\nitem once played), repeat (looping through the playlist), random (randomize the playlist),\nautoplay (randomly grab something from the music library).
\n- !duck on/off - enable or disable ducking function
\n- !duckv {volume} - set the volume of the bot when ducking is activated
\n- !duckthres - set the threshold of volume to activate ducking (3000 by default)
\n- !oust - stop playing and go to default channel
\n
\nPlaylist\n\n- !now (or !np) - display the current song
\n- !queue - display items in the playlist
\n- !tag {tags} - add all items with tags {tags}, tags separated by \",\".
\n- !file (or !f) {path/folder/keyword} - add a single file to the playlist by its path or keyword in its path.
\n- !filematch (or !fm) {pattern} - add all files that match regex {pattern}
\n- !url {url} - add Youtube or SoundCloud music
\n- !playlist {url} [{offset}] - add all items in a Youtube or SoundCloud playlist, and start with the {offset}-th item
\n- !radio {url} - append a radio {url} to the playlist
\n- !rbquery {keyword} - query http://www.radio-browser.info for a radio station
\n- !rbplay {id} - play a radio station with {id} (eg. !rbplay 96746)
\n- !ysearch {keywords} - query youtube. Use !ysearch -n to turn the page.
\n- !yplay {keywords} - add the first search result of {keywords} into the playlist.
\n- !shortlist (or !sl) {indexes/*} - add {indexes}-th item (or all items if * is given) on the shortlist.
\n- !rm {num} - remove the num-th song on the playlist
\n- !repeat [{num}] - repeat current song {num} (1 by default) times.
\n- !random - randomize the playlist.
\n
\nMusic Library\n\n- !search {keywords} - find item with {keywords} in the music library, keywords separated by space.
\n- !listfile [{pattern}] - display list of available files (whose paths match the regex pattern if {pattern} is given)
\n- !addtag [{index}] {tags} - add {tags} to {index}-th(current song if {index} is omitted) item on the playlist, tags separated by \",\".
\n- !addtag * {tags} - add {tags} to all items on the playlist.
\n- !untag [{index/*}] {tags}/* - remove {tags}/all tags from {index}-th(current song if {index} is omitted) item on the playlist.
\n- !findtagged (or !ft) {tags} - find item with {tags} in the music library.
\n- !delete {index} - delete {index}-th item on the shortlist from the music library.
\n
\nOther\n\n- !joinme {token} - join your own channel with {token}.
\n- !password {password} - change your password, used to access the web interface.
\n
",
+ "help": "Commands
\nControl\n\n- !play (or !p) [{num}] [{start_from}] - resume from pausing / start to play (the num-th song is num if given)
\n- !pause - pause
\n- !stop - stop playing
\n- !skip - jump to the next song
\n- !last - jump to the last song
\n- !volume {volume} - get or change the volume (from 0 to 100)
\n- !mode [{mode}] - get or set the playback mode, {mode} should be one of one-shot (remove\nitem once played), repeat (looping through the playlist), random (randomize the playlist),\nautoplay (randomly grab something from the music library).
\n- !duck on/off - enable or disable ducking function
\n- !duckv {volume} - set the volume of the bot when ducking is activated
\n- !duckthres - set the threshold of volume to activate ducking (3000 by default)
\n- !oust - stop playing and go to default channel
\n
\nPlaylist\n\n- !now (or !np) - display the current song
\n- !queue - display items in the playlist
\n- !tag {tags} - add all items with tags {tags}, tags separated by \",\".
\n- !file (or !f) {path/folder/keyword} - add a single file to the playlist by its path or keyword in its path.
\n- !filematch (or !fm) {pattern} - add all files that match regex {pattern}
\n- !url {url} - add Youtube or SoundCloud music
\n- !playlist {url} [{offset}] - add all items in a Youtube or SoundCloud playlist, and start with the {offset}-th item
\n- !radio {url} - append a radio {url} to the playlist
\n- !rbquery {keyword} - query http://www.radio-browser.info for a radio station
\n- !rbplay {id} - play a radio station with {id} (eg. !rbplay 96746)
\n- !ysearch {keywords} - query youtube. Use !ysearch -n to turn the page.
\n- !yplay {keywords} - add the first search result of {keywords} into the playlist.
\n- !shortlist (or !sl) {indexes/*} - add {indexes}-th item (or all items if * is given) on the shortlist.
\n- !rm {num} - remove the num-th song on the playlist
\n- !repeat [{num}] - repeat current song {num} (1 by default) times.
\n- !random - randomize the playlist.
\n
\nMusic Library\n\n- !search {keywords} - find item with {keywords} in the music library, keywords separated by space.
\n- !listfile [{pattern}] - display list of available files (whose paths match the regex pattern if {pattern} is given)
\n- !addtag [{index}] {tags} - add {tags} to {index}-th(current song if {index} is omitted) item on the playlist, tags separated by \",\".
\n- !addtag * {tags} - add {tags} to all items on the playlist.
\n- !untag [{index/*}] {tags}/* - remove {tags}/all tags from {index}-th(current song if {index} is omitted) item on the playlist.
\n- !findtagged (or !ft) {tags} - find item with {tags} in the music library.
\n- !delete {index} - delete {index}-th item on the shortlist from the music library.
\n
\nOther\n\n- !joinme {token} - join your own channel with {token}.
\n
",
"invalid_index": "Invalid index {index}. Use !queue to see the playlist.",
"last_song_on_the_queue": "Last one on the queue.",
"max_volume": "Volume exceeds max volume of {max}. Setting volume to max.",
@@ -83,91 +83,11 @@
"user_ban": "You are banned, not allowed to do that!",
"user_ban_list": "List of banned user:
{list}",
"user_ban_success": "User {user} is banned.",
- "user_password_set": "Your password has been updated.",
"user_unban_success": "User {user} is unbanned.",
- "web_user_list": "Following users have the privilege to access the web interface:
{users}",
- "webpage_address": "Your own address to access the web interface is {address}",
"which_command": "Do you mean
{commands}",
"wrong_pattern": "Invalid regex: {error}.",
"yt_no_more": "No more results!",
"yt_query_error": "Unable to query youtube!",
"yt_result": "Youtube query result: {result_table} Use !sl {{indexes}} to play the item you want.
\n!ytquery -n for the next page."
- },
- "web": {
- "action": "Action",
- "add": "Add",
- "add_all": "Add All",
- "add_radio": "Add Radio",
- "add_radio_url": "Add Radio URL",
- "add_to_bottom": "Add to bottom",
- "add_to_bottom_of_current_playlist": "Add to bottom of current playlist",
- "add_to_playlist_next": "Add to playlist right after current song",
- "add_url": "Add URL",
- "add_youtube_or_soundcloud_url": "Add Youtube or Soundcloud URL",
- "are_you_really_sure": "Are you really sure?",
- "aria_bragi_logo": "Botamusique Logo: a fox with two headphones, enjoying the music",
- "aria_default_cover": "A black square with two eighth notes beamed together.",
- "aria_empty_box": "A drawing of an empty box.",
- "aria_remove_this_song": "Remove this song from the current playlist",
- "aria_skip_current_song": "Skip current song and play this song right now",
- "aria_skip_to_next_track": "Skip to next track",
- "aria_spinner": "A loading spinner",
- "aria_warning_of_deletion": "Warning about deletion of files.",
- "autoplay": "Autoplay",
- "browse_music_file": "Browse Music file",
- "cancel": "Cancel",
- "cancel_upload_warning": "Are you really sure?
Click again to abort uploading.",
- "change_playback_mode": "Change Playback Mode",
- "choose_file": "Choose file",
- "clear_playlist": "Clear Playlist",
- "close": "Close",
- "delete_all": "Delete All",
- "delete_all_files": "Delete All Listed Files",
- "delete_file_warning": "All files listed here, include files on other pages, will be deleted from your hard-drive.\n Is that what you want?",
- "directory": "Directory",
- "download_all": "Download All",
- "download_song_from_library": "Download song from library",
- "edit_submit": "Edit!",
- "edit_tags_for": "Edit tags for",
- "expand_playlist": "See item on the playlist.",
- "file": "File",
- "filters": "Filters",
- "index": "#",
- "keywords": "Keywords",
- "keywords_placeholder": "Keywords...",
- "mini_player_title": "Now Playing...",
- "music_library": "Music Library",
- "next_to_play": "Next to play",
- "no_tag": "No tag",
- "oneshot": "One-shot",
- "open_volume_controls": "Open Volume Controls",
- "page_title": "bragi Web Interface",
- "pause": "Pause",
- "play": "Play",
- "playlist_controls": "Playlist controls",
- "radio": "Radio",
- "radio_url_placeholder": "Radio URL...",
- "random": "Random",
- "remove_song_from_library": "Remove song from library",
- "repeat": "Repeat",
- "rescan_files": "Rescan Files",
- "skip_track": "Skip Track",
- "submit": "Submit",
- "tags": "Tags",
- "tags_to_add": "Tags to add",
- "title": "Title",
- "token": "Token",
- "token_required": "Token Required",
- "token_required_message": "You are accessing the web interface of {{ name }}.\nA token is needed to grant you access.
\nPlease send \"{{ command }}\" to the bot in mumble to acquire one.",
- "type": "Type",
- "upload_file": "Upload File",
- "upload_submit": "Upload!",
- "upload_to": "Upload To",
- "uploaded_finished": "Uploaded finished!",
- "uploading_files": "Uploading files...",
- "url": "URL",
- "url_path": "Url/Path",
- "url_placeholder": "URL...",
- "volume_slider": "Volume Slider"
}
-}
\ No newline at end of file
+}
diff --git a/tests/test_volume_restore.py b/tests/test_volume_restore.py
new file mode 100644
index 0000000..282b24d
--- /dev/null
+++ b/tests/test_volume_restore.py
@@ -0,0 +1,206 @@
+import configparser
+import logging
+import os
+import tempfile
+import types
+import unittest
+from unittest import mock
+
+import bragi
+import constants
+from database import DatabaseMigration, MusicDatabase, SettingsDatabase
+import command
+import util
+import variables as var
+
+
+class FakeBot:
+ def __init__(self):
+ self.registered = []
+
+ def register_command(self, cmd, handle, no_partial_match=False, access_outside_channel=False, admin=False):
+ self.registered.append(cmd)
+
+
+class FakePlaylist:
+ def __init__(self, item=None):
+ self.item = item
+
+ def __len__(self):
+ return 1 if self.item is not None else 0
+
+ def current_item(self):
+ return self.item
+
+
+class ExhaustedPlaylist:
+ current_index = -1
+
+ def next(self):
+ return False
+
+
+class FakeSoundOutput:
+ def get_buffer_size(self):
+ return 0
+
+
+class FakeMumble:
+ def __init__(self):
+ self.sound_output = FakeSoundOutput()
+ self.alive_checks = 0
+
+ def is_alive(self):
+ self.alive_checks += 1
+ return self.alive_checks < 2
+
+
+class VolumeRestoreTests(unittest.TestCase):
+ def setUp(self):
+ settings_file = tempfile.NamedTemporaryFile(delete=False)
+ settings_file.close()
+ music_file = tempfile.NamedTemporaryFile(delete=False)
+ music_file.close()
+ self.settings_path = settings_file.name
+ self.music_path = music_file.name
+
+ self.settings_db = SettingsDatabase(self.settings_path)
+ self.music_db = MusicDatabase(self.music_path)
+ DatabaseMigration(self.settings_db, self.music_db).migrate()
+
+ self.config = configparser.ConfigParser(interpolation=None, allow_no_value=True)
+ self.config.read("configuration.default.ini", encoding="utf-8")
+ var.config = self.config
+ var.db = self.settings_db
+ var.music_db = self.music_db
+ var.cache = None
+ var.playlist = []
+
+ def tearDown(self):
+ os.unlink(self.settings_path)
+ os.unlink(self.music_path)
+
+ def make_bot(self):
+ bot = bragi.MumbleBot.__new__(bragi.MumbleBot)
+ bot.volume_helper = util.VolumeHelper()
+ bot.volume_helper.set_volume(1.0)
+ bot.thread = None
+ bot.on_interrupting = False
+ bot.song_start_at = -1
+ bot.read_pcm_size = 0
+ bot.is_pause = False
+ bot.wait_for_ready = False
+ bot.log = logging.getLogger("test")
+ return bot
+
+ def test_restore_volume_on_stop_updates_runtime_and_db_from_config(self):
+ self.config.set("bot", "restore_volume_on_stop", "True")
+ self.config.set("bot", "volume", "0.7")
+ self.settings_db.set("bot", "volume", "1.0")
+
+ bot = self.make_bot()
+
+ bot.restore_default_volume()
+
+ self.assertAlmostEqual(bot.volume_helper.plain_volume_set, 0.7)
+ self.assertAlmostEqual(self.settings_db.getfloat("bot", "volume"), 0.7)
+
+ def test_restore_volume_on_stop_respects_max_volume_limit(self):
+ self.config.set("bot", "restore_volume_on_stop", "True")
+ self.config.set("bot", "volume", "0.9")
+ self.settings_db.set("bot", "max_volume", "0.5")
+
+ bot = self.make_bot()
+
+ bot.restore_default_volume()
+
+ self.assertAlmostEqual(bot.volume_helper.plain_volume_set, 0.5)
+ self.assertAlmostEqual(self.settings_db.getfloat("bot", "volume"), 0.5)
+
+ def test_restore_volume_on_stop_is_disabled_by_default(self):
+ self.config.set("bot", "restore_volume_on_stop", "False")
+ self.config.set("bot", "volume", "0.7")
+ self.settings_db.set("bot", "volume", "1.0")
+
+ bot = self.make_bot()
+
+ bot.restore_default_volume()
+
+ self.assertAlmostEqual(bot.volume_helper.plain_volume_set, 1.0)
+ self.assertAlmostEqual(self.settings_db.getfloat("bot", "volume"), 1.0)
+
+ def test_stop_restores_default_volume(self):
+ self.config.set("bot", "restore_volume_on_stop", "True")
+ var.playlist = [object()]
+
+ bot = self.make_bot()
+ bot.restore_default_volume = mock.Mock()
+ bot._cleanup_ffmpeg_process = mock.Mock()
+
+ bot.stop()
+
+ bot.restore_default_volume.assert_called_once_with()
+
+ def test_pause_does_not_restore_default_volume(self):
+ var.playlist = FakePlaylist(types.SimpleNamespace(id="track-id"))
+
+ bot = self.make_bot()
+ bot.playhead = 12.5
+ bot.restore_default_volume = mock.Mock()
+
+ bot.pause()
+
+ bot.restore_default_volume.assert_not_called()
+
+ def test_loop_restores_volume_without_entering_pause_when_queue_runs_empty(self):
+ var.playlist = ExhaustedPlaylist()
+
+ bot = self.make_bot()
+ bot.mumble = FakeMumble()
+ bot.restore_default_volume = mock.Mock()
+ bot._cleanup_ffmpeg_process = mock.Mock()
+ bot.exit = False
+ bot.last_ffmpeg_err = ""
+ bot.pcm_buffer_size = 1
+ bot.read_pcm_size = 1
+ bot._loop_status = ""
+
+ bragi.MumbleBot.loop(bot)
+
+ bot.restore_default_volume.assert_called_once_with()
+ self.assertFalse(bot.is_pause)
+
+
+class HelpTextTests(unittest.TestCase):
+ def test_help_text_does_not_reference_web_interface(self):
+ constants.load_lang("en_US")
+
+ help_text = constants.tr_cli("help")
+ admin_help = constants.tr_cli("admin_help")
+
+ self.assertNotIn("!web", help_text)
+ self.assertNotIn("web interface", help_text.lower())
+ self.assertNotIn("web interface", admin_help.lower())
+
+
+class CommandRegistrationTests(unittest.TestCase):
+ def setUp(self):
+ self.config = configparser.ConfigParser(interpolation=None, allow_no_value=True)
+ self.config.read("configuration.default.ini", encoding="utf-8")
+ var.config = self.config
+
+ def test_register_all_commands_excludes_web_commands(self):
+ bot = FakeBot()
+
+ command.register_all_commands(bot)
+
+ registered = ",".join(bot.registered)
+ self.assertIn(self.config.get("commands", "volume"), registered)
+ self.assertNotIn("web", registered)
+ self.assertNotIn("webuseradd", registered)
+ self.assertNotIn("webuserlist", registered)
+ self.assertNotIn("webuserdel", registered)
+
+
+if __name__ == "__main__":
+ unittest.main()