From d7af7baa618bde205e07ce0f38fb5fa576bf24b2 Mon Sep 17 00:00:00 2001 From: cottongin Date: Tue, 25 Feb 2025 00:00:45 -0800 Subject: [PATCH] add quiet/unquiet commands to suppress song change announcements --- README.md | 15 ++++++ config.yaml.example | 1 + icecast-irc-bot-manager.py | 96 +++++++++++++++++++++++++++++++++++++- install.sh | 4 +- main.py | 86 ++++++++++++++++++++++++++++++++++ 5 files changed, 199 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 854088a..12973bf 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,19 @@ admin: ## 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 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 - `!stop` - Stop stream monitoring +- `!quiet` - Disable song announcements but continue monitoring +- `!unquiet` - Enable song announcements - `!reconnect` - Reconnect to the stream - `!restart` - Restart the bot - `!quit` - Shutdown the bot diff --git a/config.yaml.example b/config.yaml.example index a56efc4..dcc7bfd 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -17,6 +17,7 @@ announce: - "Unknown" - "Unable to fetch metadata" - "Error fetching metadata" + quiet_on_start: false # If true, bot starts in quiet mode (no announcements) commands: prefix: "!" # Command prefix (e.g. !np, !help) diff --git a/icecast-irc-bot-manager.py b/icecast-irc-bot-manager.py index d9eaa14..cb89853 100644 --- a/icecast-irc-bot-manager.py +++ b/icecast-irc-bot-manager.py @@ -388,6 +388,76 @@ class BotManager: # Start the bot with the same config 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: """List all running bots. @@ -465,8 +535,8 @@ class BotManager: async def main(): parser = argparse.ArgumentParser(description='Icecast IRC Bot Manager') 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('bot_id', nargs='?', help='Bot ID for start/stop/restart commands, or "all" to stop all bots') + 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/quiet/unquiet commands, or "all" to stop all bots') args = parser.parse_args() @@ -538,6 +608,28 @@ async def main(): break except asyncio.CancelledError: 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: should_cleanup = True # Need cleanup for keyboard interrupt diff --git a/install.sh b/install.sh index 8b89e05..86043f7 100755 --- a/install.sh +++ b/install.sh @@ -68,4 +68,6 @@ echo "Usage:" 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 stop BOT_ID # Stop a bot" -echo "icecast-irc-bot-manager restart BOT_ID # Restart a bot" \ No newline at end of file +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" \ No newline at end of file diff --git a/main.py b/main.py index 635a76a..d8f2ae8 100755 --- a/main.py +++ b/main.py @@ -90,6 +90,8 @@ class RestartManager: self.socket_path = Path(tempfile.gettempdir()) / f"icecast_bot_{bot_id}.sock" self.server = None self.should_restart = False + self.quiet_requested = False + self.unquiet_requested = False async def start(self): """Start the restart manager server.""" @@ -119,6 +121,12 @@ class RestartManager: if data == b'restart': self.logger.info("Received restart request") 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() await writer.wait_closed() except Exception as e: @@ -256,6 +264,9 @@ class IcecastBot: self.monitor_task = None self.should_monitor = True 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.current_process = None self.should_exit = False @@ -634,6 +645,40 @@ class IcecastBot: await message.recipient.message("Stream monitoring started.") 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): """Build bidirectional mappings between command patterns and method names. @@ -1018,9 +1063,14 @@ class IcecastBot: - The channel object is valid - The song doesn't match any ignore patterns - The announcement format is configured + - The should_announce flag is True """ try: 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): self.logger.debug(f"Song passed filters, preparing to announce") # 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}") try: 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() 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 if bot.restart_manager.should_restart: bot.logger.info("Bot should restart")