add quiet/unquiet commands to suppress song change announcements

This commit is contained in:
cottongin 2025-02-25 00:00:45 -08:00
parent 9c978c4773
commit d7af7baa61
Signed by: cottongin
GPG Key ID: A0BD18428A296890
5 changed files with 199 additions and 3 deletions

View File

@ -73,6 +73,19 @@ admin:
## Usage ## Usage
## Manager Commands
The bot manager supports the following commands:
```bash
icecast-irc-bot-manager list # List running bots
icecast-irc-bot-manager start --config FILE # Start a bot
icecast-irc-bot-manager stop BOT_ID # Stop a bot
icecast-irc-bot-manager restart BOT_ID # Restart a bot
icecast-irc-bot-manager quiet BOT_ID # Disable song announcements
icecast-irc-bot-manager unquiet BOT_ID # Enable song announcements
```
### Running with Automatic Restart Support ### Running with Automatic Restart Support
The recommended way to run the bot is using the provided `bot.sh` script, which handles automatic restarts when using the `!restart` command: The recommended way to run the bot is using the provided `bot.sh` script, which handles automatic restarts when using the `!restart` command:
@ -132,6 +145,8 @@ Admin commands (only available to users listed in the `admin.users` config):
- `!start` - Start stream monitoring - `!start` - Start stream monitoring
- `!stop` - Stop stream monitoring - `!stop` - Stop stream monitoring
- `!quiet` - Disable song announcements but continue monitoring
- `!unquiet` - Enable song announcements
- `!reconnect` - Reconnect to the stream - `!reconnect` - Reconnect to the stream
- `!restart` - Restart the bot - `!restart` - Restart the bot
- `!quit` - Shutdown the bot - `!quit` - Shutdown the bot

View File

@ -17,6 +17,7 @@ announce:
- "Unknown" - "Unknown"
- "Unable to fetch metadata" - "Unable to fetch metadata"
- "Error fetching metadata" - "Error fetching metadata"
quiet_on_start: false # If true, bot starts in quiet mode (no announcements)
commands: commands:
prefix: "!" # Command prefix (e.g. !np, !help) prefix: "!" # Command prefix (e.g. !np, !help)

View File

@ -388,6 +388,76 @@ class BotManager:
# Start the bot with the same config # Start the bot with the same config
return await self.start_bot(config_path) return await self.start_bot(config_path)
async def quiet_bot(self, bot_id: str) -> bool:
"""Disable song announcements for a running bot.
Args:
bot_id: ID of the bot to quiet
Returns:
bool: True if command was sent successfully
"""
# Check if bot exists in state
state = self._load_state()
if bot_id not in state:
print(f"Bot {bot_id} not found")
return False
try:
# Get the socket path for this bot
socket_path = Path(tempfile.gettempdir()) / f"icecast_bot_{bot_id}.sock"
if not socket_path.exists():
print(f"Socket for bot {bot_id} not found at {socket_path}")
return False
# Send quiet command
print(f"Sending quiet command to bot {bot_id}")
reader, writer = await asyncio.open_unix_connection(str(socket_path))
writer.write(b'quiet')
await writer.drain()
writer.close()
await writer.wait_closed()
print(f"Quiet command sent to bot {bot_id}")
return True
except Exception as e:
print(f"Error sending quiet command to bot {bot_id}: {e}")
return False
async def unquiet_bot(self, bot_id: str) -> bool:
"""Enable song announcements for a running bot.
Args:
bot_id: ID of the bot to unquiet
Returns:
bool: True if command was sent successfully
"""
# Check if bot exists in state
state = self._load_state()
if bot_id not in state:
print(f"Bot {bot_id} not found")
return False
try:
# Get the socket path for this bot
socket_path = Path(tempfile.gettempdir()) / f"icecast_bot_{bot_id}.sock"
if not socket_path.exists():
print(f"Socket for bot {bot_id} not found at {socket_path}")
return False
# Send unquiet command
print(f"Sending unquiet command to bot {bot_id}")
reader, writer = await asyncio.open_unix_connection(str(socket_path))
writer.write(b'unquiet')
await writer.drain()
writer.close()
await writer.wait_closed()
print(f"Unquiet command sent to bot {bot_id}")
return True
except Exception as e:
print(f"Error sending unquiet command to bot {bot_id}: {e}")
return False
async def list_bots(self) -> bool: async def list_bots(self) -> bool:
"""List all running bots. """List all running bots.
@ -465,8 +535,8 @@ class BotManager:
async def main(): async def main():
parser = argparse.ArgumentParser(description='Icecast IRC Bot Manager') parser = argparse.ArgumentParser(description='Icecast IRC Bot Manager')
parser.add_argument('--config', type=str, help='Path to config file or directory') parser.add_argument('--config', type=str, help='Path to config file or directory')
parser.add_argument('command', choices=['start', 'stop', 'restart', 'list'], help='Command to execute') parser.add_argument('command', choices=['start', 'stop', 'restart', 'list', 'quiet', 'unquiet'], help='Command to execute')
parser.add_argument('bot_id', nargs='?', help='Bot ID for start/stop/restart commands, or "all" to stop all bots') parser.add_argument('bot_id', nargs='?', help='Bot ID for start/stop/restart/quiet/unquiet commands, or "all" to stop all bots')
args = parser.parse_args() args = parser.parse_args()
@ -538,6 +608,28 @@ async def main():
break break
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
elif args.command == 'quiet':
should_cleanup = False # Don't need cleanup for quiet command
if not args.bot_id:
print("Error: bot_id required for quiet command")
sys.exit(1)
if args.bot_id == "all":
print("Error: quiet all is not supported")
sys.exit(1)
if not await manager.quiet_bot(args.bot_id):
sys.exit(1)
elif args.command == 'unquiet':
should_cleanup = False # Don't need cleanup for unquiet command
if not args.bot_id:
print("Error: bot_id required for unquiet command")
sys.exit(1)
if args.bot_id == "all":
print("Error: unquiet all is not supported")
sys.exit(1)
if not await manager.unquiet_bot(args.bot_id):
sys.exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
should_cleanup = True # Need cleanup for keyboard interrupt should_cleanup = True # Need cleanup for keyboard interrupt

View File

@ -68,4 +68,6 @@ echo "Usage:"
echo "icecast-irc-bot-manager list # List running bots" echo "icecast-irc-bot-manager list # List running bots"
echo "icecast-irc-bot-manager start --config FILE # Start a bot" echo "icecast-irc-bot-manager start --config FILE # Start a bot"
echo "icecast-irc-bot-manager stop BOT_ID # Stop a bot" echo "icecast-irc-bot-manager stop BOT_ID # Stop a bot"
echo "icecast-irc-bot-manager restart BOT_ID # Restart a bot" echo "icecast-irc-bot-manager restart BOT_ID # Restart a bot"
echo "icecast-irc-bot-manager quiet BOT_ID # Disable song announcements"
echo "icecast-irc-bot-manager unquiet BOT_ID # Enable song announcements"

86
main.py
View File

@ -90,6 +90,8 @@ class RestartManager:
self.socket_path = Path(tempfile.gettempdir()) / f"icecast_bot_{bot_id}.sock" self.socket_path = Path(tempfile.gettempdir()) / f"icecast_bot_{bot_id}.sock"
self.server = None self.server = None
self.should_restart = False self.should_restart = False
self.quiet_requested = False
self.unquiet_requested = False
async def start(self): async def start(self):
"""Start the restart manager server.""" """Start the restart manager server."""
@ -119,6 +121,12 @@ class RestartManager:
if data == b'restart': if data == b'restart':
self.logger.info("Received restart request") self.logger.info("Received restart request")
self.should_restart = True self.should_restart = True
elif data == b'quiet':
self.logger.info("Received quiet request")
self.quiet_requested = True
elif data == b'unquiet':
self.logger.info("Received unquiet request")
self.unquiet_requested = True
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
except Exception as e: except Exception as e:
@ -256,6 +264,9 @@ class IcecastBot:
self.monitor_task = None self.monitor_task = None
self.should_monitor = True self.should_monitor = True
self.is_monitoring = False self.is_monitoring = False
self.should_announce = not self.config.get('announce', {}).get('quiet_on_start', False) # Respect quiet_on_start config
if not self.should_announce:
self.logger.info("Starting in quiet mode (announcements disabled) due to configuration")
self.admin_users = self.config.get('admin', {}).get('users', ['*']) self.admin_users = self.config.get('admin', {}).get('users', ['*'])
self.current_process = None self.current_process = None
self.should_exit = False self.should_exit = False
@ -634,6 +645,40 @@ class IcecastBot:
await message.recipient.message("Stream monitoring started.") await message.recipient.message("Stream monitoring started.")
self.command_handlers['start_monitoring'] = start_monitoring self.command_handlers['start_monitoring'] = start_monitoring
@self.bot.on_message(create_command_pattern('quiet'))
@self.admin_required
async def quiet_bot(message):
"""!quiet: Disable song announcements (admin only)
Continues monitoring the stream for metadata changes,
but stops announcing songs in the channel.
Args:
message: IRC message object
"""
self.should_announce = False
self.logger.info("Song announcements disabled by admin command")
if hasattr(message.recipient, 'name') and message.recipient.name.startswith('#'):
await message.recipient.message("Song announcements disabled. Bot will continue monitoring but remain quiet.")
self.command_handlers['quiet_bot'] = quiet_bot
@self.bot.on_message(create_command_pattern('unquiet'))
@self.admin_required
async def unquiet_bot(message):
"""!unquiet: Enable song announcements (admin only)
Resumes announcing songs in the channel.
The bot must already be monitoring the stream.
Args:
message: IRC message object
"""
self.should_announce = True
self.logger.info("Song announcements enabled by admin command")
if hasattr(message.recipient, 'name') and message.recipient.name.startswith('#'):
await message.recipient.message("Song announcements enabled. Bot will now announce songs.")
self.command_handlers['unquiet_bot'] = unquiet_bot
def _build_command_mappings(self): def _build_command_mappings(self):
"""Build bidirectional mappings between command patterns and method names. """Build bidirectional mappings between command patterns and method names.
@ -1018,9 +1063,14 @@ class IcecastBot:
- The channel object is valid - The channel object is valid
- The song doesn't match any ignore patterns - The song doesn't match any ignore patterns
- The announcement format is configured - The announcement format is configured
- The should_announce flag is True
""" """
try: try:
self.logger.info(f"Attempting to announce song: {song}") self.logger.info(f"Attempting to announce song: {song}")
if not self.should_announce:
self.logger.info(f"Song announcements are disabled, not announcing: {song}")
return
if self.channel and self.should_announce_song(song): if self.channel and self.should_announce_song(song):
self.logger.debug(f"Song passed filters, preparing to announce") self.logger.debug(f"Song passed filters, preparing to announce")
# Use the stored channel object directly # Use the stored channel object directly
@ -1257,9 +1307,45 @@ async def run_single_bot(bot: IcecastBot):
bot.logger.info(f"Running single bot with ID: {bot.bot_id}") bot.logger.info(f"Running single bot with ID: {bot.bot_id}")
try: try:
bot.logger.info("Starting bot...") bot.logger.info("Starting bot...")
# Start a background task to check for quiet/unquiet requests
async def check_quiet_unquiet():
while True:
try:
# Check for quiet request
if bot.restart_manager.quiet_requested:
bot.logger.info("Processing quiet request")
bot.should_announce = False
bot.restart_manager.quiet_requested = False
if bot.channel and hasattr(bot.channel, 'name') and bot.channel.name.startswith('#'):
await bot.channel.message("Song announcements disabled via terminal command.")
# Check for unquiet request
if bot.restart_manager.unquiet_requested:
bot.logger.info("Processing unquiet request")
bot.should_announce = True
bot.restart_manager.unquiet_requested = False
if bot.channel and hasattr(bot.channel, 'name') and bot.channel.name.startswith('#'):
await bot.channel.message("Song announcements enabled via terminal command.")
except Exception as e:
bot.logger.error(f"Error in quiet/unquiet check: {e}")
await asyncio.sleep(1) # Check every second
# Start the background task
quiet_check_task = asyncio.create_task(check_quiet_unquiet())
# Start the bot
await bot.start() await bot.start()
bot.logger.info("Bot start completed") bot.logger.info("Bot start completed")
# Cancel the quiet check task
quiet_check_task.cancel()
try:
await quiet_check_task
except asyncio.CancelledError:
pass
# Check if we should restart this bot # Check if we should restart this bot
if bot.restart_manager.should_restart: if bot.restart_manager.should_restart:
bot.logger.info("Bot should restart") bot.logger.info("Bot should restart")