Added ability to specify custom sounds for users when they log in.
This commit is contained in:
@@ -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
|
||||||
|
# USERUPDATED and USERREMOVED pass (user, action)
|
||||||
user_change_callback = \
|
user_change_callback = \
|
||||||
lambda user, action: threading.Thread(target=self.users_changed,
|
lambda user, action: threading.Thread(target=self.users_changed,
|
||||||
args=(user, action), daemon=True).start()
|
args=(user, action), daemon=True).start()
|
||||||
|
# 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_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_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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user