Added ability to specify custom sounds for users when they log in.

This commit is contained in:
Storm Dragon
2025-11-22 18:31:22 -05:00
parent 769f59731d
commit 7e37570b43
3 changed files with 108 additions and 12 deletions
+90 -8
View File
@@ -33,9 +33,9 @@ import command
import constants import constants
import media.playlist import media.playlist
from constants import tr_cli as tr from constants import tr_cli as tr
from database import SettingsDatabase, MusicDatabase, DatabaseMigration from database import SettingsDatabase, MusicDatabase, DatabaseMigration, Condition
from media.item import ValidationFailedError, PreparationFailedError from media.item import ValidationFailedError, PreparationFailedError
from media.cache import MusicCache from media.cache import MusicCache, get_cached_wrapper_from_scrap, get_cached_wrapper_from_dict
class MumbleBot: class MumbleBot:
@@ -76,6 +76,7 @@ class MumbleBot:
# #
self.on_interrupting = False self.on_interrupting = False
self.is_ready = False # Flag to prevent join sounds during bot initialization
if args.host: if args.host:
host = args.host host = args.host
@@ -154,6 +155,10 @@ class MumbleBot:
self.bots = set(bots.split(',')) self.bots = set(bots.split(','))
self._user_in_channel = self.get_user_count_in_channel() self._user_in_channel = self.get_user_count_in_channel()
# Mark bot as ready - initialization complete, can now play join sounds
self.is_ready = True
self.log.info("bot: Initialization complete, ready to play join sounds")
# ====== Volume ====== # ====== Volume ======
self.volume_helper = util.VolumeHelper() self.volume_helper = util.VolumeHelper()
@@ -190,12 +195,20 @@ class MumbleBot:
assert var.config.get("bot", "when_nobody_in_channel") in ['pause', 'pause_resume', 'stop', 'nothing', ''], \ assert var.config.get("bot", "when_nobody_in_channel") in ['pause', 'pause_resume', 'stop', 'nothing', ''], \
"Unknown action for when_nobody_in_channel" "Unknown action for when_nobody_in_channel"
if var.config.get("bot", "when_nobody_in_channel") in ['pause', 'pause_resume', 'stop']: # Always register user change callback for join sounds and when_nobody_in_channel features
user_change_callback = \ # USERUPDATED and USERREMOVED pass (user, action)
lambda user, action: threading.Thread(target=self.users_changed, user_change_callback = \
args=(user, action), daemon=True).start() lambda user, action: threading.Thread(target=self.users_changed,
self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_USERREMOVED, user_change_callback) args=(user, action), daemon=True).start()
self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_USERUPDATED, user_change_callback) # USERCREATED only passes (user) with no action
user_created_callback = \
lambda user: threading.Thread(target=self.users_changed,
args=(user, None), 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)
self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_USERCREATED, user_created_callback)
self.log.info("bot: User change callbacks registered for join sounds")
# Debug use # Debug use
self._loop_status = 'Idle' self._loop_status = 'Idle'
@@ -389,7 +402,76 @@ class MumbleBot:
return len(users) return len(users)
def _play_join_sound(self, username):
"""Play a configured join sound for a user if bot is idle"""
self.log.debug(f"bot: _play_join_sound called for username: {username}")
# Only play if bot is idle (not currently playing music)
if self.thread is not None:
self.log.debug(f"bot: Not playing join sound for {username} - bot is currently playing music")
return
# Check if user has a configured join sound
if not var.config.has_option('user_join_sounds', username):
self.log.debug(f"bot: No join sound configured for {username}")
return
sound_config = var.config.get('user_join_sounds', username).strip()
self.log.debug(f"bot: Found join sound config for {username}: {sound_config}")
if not sound_config:
self.log.debug(f"bot: Join sound config for {username} is empty")
return
try:
# Determine if it's a URL or file path
if sound_config.startswith('http://') or sound_config.startswith('https://'):
# It's a URL
self.log.info(f'bot: Playing join sound URL for {username}: {sound_config}')
music_wrapper = get_cached_wrapper_from_scrap(type='url', url=sound_config, user='system')
else:
# It's a file path - search database for first match
matches = var.music_db.query_music(Condition()
.and_equal('type', 'file')
.and_like('path', '%' + sound_config + '%', case_sensitive=False))
if not matches:
self.log.warning(f'bot: Join sound file not found for {username}: {sound_config}')
return
# Use first match
self.log.info(f'bot: Playing join sound file for {username}: {matches[0]["path"]}')
music_wrapper = get_cached_wrapper_from_dict(matches[0], 'system')
# Add to playlist
var.playlist.append(music_wrapper)
except Exception as e:
self.log.error(f'bot: Error playing join sound for {username}: {e}')
def users_changed(self, user, message): def users_changed(self, user, message):
self.log.info(f"bot: users_changed called - user: {user.get('name', 'unknown')}, message type: {type(message)}, message: {message}")
# Don't play join sounds during bot initialization
if not self.is_ready:
self.log.debug(f"bot: Skipping join sound check - bot not ready yet")
# Continue with normal user count logic below
# Check if user joined/created in the bot's channel
# For USERCREATED: message is None (new user connecting)
# For USERUPDATED: message is the actions dict containing changed fields
elif message is None:
# User was created (connected to server), check if they're in our channel
bot_channel_id = self.mumble.users.myself['channel_id']
if user.get('channel_id') == bot_channel_id and user.get('name') != self.mumble.users.myself.get('name'):
self.log.info(f"bot: User {user['name']} connected in our channel, attempting to play join sound")
self._play_join_sound(user['name'])
elif isinstance(message, dict) and 'channel_id' in message:
# User changed channels
bot_channel_id = self.mumble.users.myself['channel_id']
self.log.debug(f"bot: User {user.get('name', 'unknown')} changed to channel {user['channel_id']}, bot is in {bot_channel_id}")
if user['channel_id'] == bot_channel_id and user.get('name') != self.mumble.users.myself.get('name'):
# User joined our channel, check for join sound
self.log.info(f"bot: User {user['name']} joined our channel, attempting to play join sound")
self._play_join_sound(user['name'])
# only check if there is one more user currently in the channel # 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 # else when the music is paused and somebody joins, music would start playing again
user_count = self.get_user_count_in_channel() user_count = self.get_user_count_in_channel()
+14
View File
@@ -137,3 +137,17 @@ volume = volume
yt_play = yplay yt_play = yplay
yt_search = ysearch yt_search = ysearch
[user_join_sounds]
# Play a sound when a specific user joins the channel where the bot is located
# Sounds only play when the bot is idle (not currently playing music)
# Format: username = file_path_or_url
#
# Examples:
# Storm = misc/welcome.opus
# Jack = https://site.com/soundboard/howling.wav
# Alice = sounds/hello.mp3
#
# 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
+4 -4
View File
@@ -400,12 +400,12 @@ def get_supported_language():
def set_logging_formatter(handler: logging.Handler, logging_level): def set_logging_formatter(handler: logging.Handler, logging_level):
if logging_level == logging.DEBUG: if logging_level == logging.DEBUG:
formatter = logging.Formatter( formatter = logging.Formatter(
"[%(asctime)s] > [%(threadName)s] > " "%(message)s [%(asctime)s] > [%(threadName)s] > "
"[%(filename)s:%(lineno)d] %(message)s" "[%(filename)s:%(lineno)d]"
) )
else: else:
formatter = logging.Formatter( formatter = logging.Formatter(
'[%(asctime)s %(levelname)s] %(message)s', "%b %d %H:%M:%S") '%(message)s [%(asctime)s %(levelname)s]', "%b %d %H:%M:%S")
handler.setFormatter(formatter) handler.setFormatter(formatter)
@@ -534,7 +534,7 @@ def check_extra_config(config, template):
extra = [] extra = []
for key in config.sections(): for key in config.sections():
if key in ['radio']: if key in ['radio', 'user_join_sounds']:
continue continue
for opt in config.options(key): for opt in config.options(key):
if not template.has_option(key, opt): if not template.has_option(key, opt):