Compare commits
3 Commits
v1.0.0
...
252fdf4db1
| Author | SHA1 | Date | |
|---|---|---|---|
|
252fdf4db1
|
|||
|
6817437380
|
|||
|
3cd3e98094
|
61
CHANGELOG.md
Normal file
61
CHANGELOG.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.0.1] - 2024-02-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Configurable logging levels via config.yaml
|
||||||
|
- Smart URL resolution for metadata fetching
|
||||||
|
- Connection status verification and reporting
|
||||||
|
- Admin command system with permissions
|
||||||
|
- Automatic restart functionality via bot.sh
|
||||||
|
- Enhanced command handling with prefix configuration
|
||||||
|
- Private message command control
|
||||||
|
- New admin commands:
|
||||||
|
- !start - Start stream monitoring
|
||||||
|
- !stop - Stop stream monitoring
|
||||||
|
- !reconnect - Reconnect to stream with status feedback
|
||||||
|
- !restart - Restart the bot (requires bot.sh)
|
||||||
|
- !quit - Shutdown the bot
|
||||||
|
- !help command showing available commands based on user permissions
|
||||||
|
- ERROR.log file for critical issues
|
||||||
|
- Detailed debug logging for troubleshooting
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved metadata fetching with multiple URL patterns
|
||||||
|
- Enhanced error handling and reporting
|
||||||
|
- Better stream connection status feedback
|
||||||
|
- More informative health check messages
|
||||||
|
- Cleaner logging output with configurable verbosity
|
||||||
|
- Updated documentation with new features and configuration options
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Metadata fetching issues with different URL patterns
|
||||||
|
- Command handling in channels vs private messages
|
||||||
|
- Stream reconnection verification
|
||||||
|
- Error reporting and logging clarity
|
||||||
|
- Configuration file structure and validation
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- Added version information
|
||||||
|
|
||||||
|
## [1.0.0] - 2024-02-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release
|
||||||
|
- Basic Icecast stream monitoring
|
||||||
|
- IRC channel announcements
|
||||||
|
- Simple command system (!np)
|
||||||
|
- Basic error handling and reconnection
|
||||||
|
- Multi-bot support
|
||||||
|
- Configuration via YAML files
|
||||||
|
|
||||||
|
[Unreleased]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.0.1...HEAD
|
||||||
|
[1.0.1]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.0.0...v1.0.1
|
||||||
|
[1.0.0]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/releases/tag/v1.0.0
|
||||||
137
DOCS.md
Normal file
137
DOCS.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Icecast IRC Bot Documentation v1.1.0
|
||||||
|
|
||||||
|
This document is automatically generated from the codebase.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
An IRC bot that monitors an Icecast stream and announces track changes.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
See `config.yaml.example` for a full example configuration file.
|
||||||
|
|
||||||
|
No command documentation available (help template not found)
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
### admin_required
|
||||||
|
|
||||||
|
Decorator to mark a command as requiring admin privileges.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `f`: The command handler function to wrap.
|
||||||
|
- `Returns`:
|
||||||
|
|
||||||
|
|
||||||
|
### announce_song
|
||||||
|
|
||||||
|
Announce a song in the IRC channel.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `song`: The song title to announce.
|
||||||
|
- `Only announces if`:
|
||||||
|
|
||||||
|
|
||||||
|
### fetch_json_metadata
|
||||||
|
|
||||||
|
Fetch metadata from the Icecast JSON status endpoint.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
str: The current song title, or an error message if fetching failed.
|
||||||
|
|
||||||
|
|
||||||
|
### format_help_section
|
||||||
|
|
||||||
|
Format a help section according to the template.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `section_config`: Configuration dictionary for the section.
|
||||||
|
- `prefix`: Command prefix to use.
|
||||||
|
- `Returns`:
|
||||||
|
- `List[str]`: List of formatted help lines for each command.
|
||||||
|
|
||||||
|
|
||||||
|
### get_version
|
||||||
|
|
||||||
|
Get the current version from VERSION file.
|
||||||
|
|
||||||
|
|
||||||
|
### help_command_fallback
|
||||||
|
|
||||||
|
Fallback help command implementation using hardcoded format.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `message`: The IRC message object that triggered this command.
|
||||||
|
|
||||||
|
|
||||||
|
### is_admin
|
||||||
|
|
||||||
|
Check if a user has admin privileges.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `user`: Full IRC user string (nickname!username@hostname) or User object.
|
||||||
|
- `Returns`:
|
||||||
|
- `bool`: True if user has admin privileges, False otherwise.
|
||||||
|
|
||||||
|
|
||||||
|
### load_config
|
||||||
|
|
||||||
|
Load and validate the bot configuration from a YAML file.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `config_path`: Path to the YAML configuration file. If None, uses default path.
|
||||||
|
- `Returns`:
|
||||||
|
- `dict`: The loaded and validated configuration dictionary with default values applied.
|
||||||
|
|
||||||
|
|
||||||
|
### monitor_metadata
|
||||||
|
|
||||||
|
Monitor the Icecast stream for metadata changes.
|
||||||
|
|
||||||
|
|
||||||
|
### restart_monitoring
|
||||||
|
|
||||||
|
Restart the metadata monitoring task and verify the reconnection.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
bool: True if reconnection was successful and verified, False otherwise.
|
||||||
|
|
||||||
|
|
||||||
|
### setup_handlers
|
||||||
|
|
||||||
|
Set up all IRC event handlers and command patterns.
|
||||||
|
|
||||||
|
|
||||||
|
### should_announce_song
|
||||||
|
|
||||||
|
Check if a song should be announced based on configured ignore patterns.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `song`: The song title to check.
|
||||||
|
- `Returns`:
|
||||||
|
- `bool`: True if the song should be announced, False if it matches any ignore patterns.
|
||||||
|
|
||||||
|
|
||||||
|
### start
|
||||||
|
|
||||||
|
Start the IRC bot and begin processing events.
|
||||||
|
|
||||||
|
|
||||||
|
### start_monitoring
|
||||||
|
|
||||||
|
Start the metadata monitoring task.
|
||||||
|
|
||||||
|
|
||||||
|
### stop_monitoring
|
||||||
|
|
||||||
|
Stop the metadata monitoring task.
|
||||||
|
|
||||||
78
README.md
78
README.md
@@ -1,5 +1,7 @@
|
|||||||
# Icecast-metadata-IRC-announcer
|
# Icecast-metadata-IRC-announcer
|
||||||
|
|
||||||
|
[](https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/releases)
|
||||||
|
|
||||||
A simple asynchronous Python bot that monitors an Icecast stream and announces track changes to an IRC channel. Supports running multiple instances with different configurations.
|
A simple asynchronous Python bot that monitors an Icecast stream and announces track changes to an IRC channel. Supports running multiple instances with different configurations.
|
||||||
|
|
||||||
Note: This is a work in progress. It has only been tested on **Python 3.12.6**.
|
Note: This is a work in progress. It has only been tested on **Python 3.12.6**.
|
||||||
@@ -10,8 +12,10 @@ Note: This is a work in progress. It has only been tested on **Python 3.12.6**.
|
|||||||
- Configurable via YAML files and command line arguments
|
- Configurable via YAML files and command line arguments
|
||||||
- Supports running multiple bot instances simultaneously
|
- Supports running multiple bot instances simultaneously
|
||||||
- Pattern-based song title filtering
|
- Pattern-based song title filtering
|
||||||
- Responds to !np commands in IRC channels
|
- Configurable logging levels and output
|
||||||
- Automatic reconnection and error recovery
|
- Smart URL resolution for metadata fetching
|
||||||
|
- Automatic reconnection and error recovery with status reporting
|
||||||
|
- Admin commands with permission system
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@@ -50,12 +54,43 @@ announce:
|
|||||||
ignore_patterns:
|
ignore_patterns:
|
||||||
- "Unknown"
|
- "Unknown"
|
||||||
- "Unable to fetch metadata"
|
- "Unable to fetch metadata"
|
||||||
# Add more patterns to ignore
|
- "Error fetching metadata"
|
||||||
|
|
||||||
|
commands:
|
||||||
|
prefix: "!" # Command prefix (e.g. !np, !help)
|
||||||
|
require_nick_prefix: false # If true, commands must be prefixed with "botname: " or "botname, "
|
||||||
|
allow_private_commands: false # If true, allows commands in private messages
|
||||||
|
|
||||||
|
admin:
|
||||||
|
users: # List of users who can use admin commands (use "*" for anyone)
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: "INFO" # Logging level: DEBUG, INFO, WARNING, ERROR, or CRITICAL
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Single Bot Mode
|
### 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make the script executable
|
||||||
|
chmod +x bot.sh
|
||||||
|
|
||||||
|
# Run a single bot
|
||||||
|
./bot.sh config.yaml
|
||||||
|
|
||||||
|
# Run multiple bots
|
||||||
|
./bot.sh config1.yaml config2.yaml config3.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Running
|
||||||
|
|
||||||
|
You can also run the bot directly with Python:
|
||||||
|
|
||||||
|
#### Single Bot Mode
|
||||||
|
|
||||||
Run with config file:
|
Run with config file:
|
||||||
```bash
|
```bash
|
||||||
@@ -75,32 +110,51 @@ Available command line arguments:
|
|||||||
- `--irc-channel`: IRC channel to join
|
- `--irc-channel`: IRC channel to join
|
||||||
- `--stream-url`: Icecast base URL
|
- `--stream-url`: Icecast base URL
|
||||||
- `--stream-endpoint`: Stream endpoint
|
- `--stream-endpoint`: Stream endpoint
|
||||||
|
- `--cmd-prefix`: Command prefix character(s)
|
||||||
|
|
||||||
### Multiple Bot Mode
|
#### Multiple Bot Mode
|
||||||
|
|
||||||
Run multiple instances with different configs:
|
Run multiple instances with different configs:
|
||||||
```bash
|
```bash
|
||||||
python main.py config1.yaml config2.yaml config3.yaml
|
python main.py config1.yaml config2.yaml config3.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use the launcher script:
|
|
||||||
```bash
|
|
||||||
python launch.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## IRC Commands
|
## IRC Commands
|
||||||
|
|
||||||
|
Regular commands:
|
||||||
- `!np`: Shows the currently playing track
|
- `!np`: Shows the currently playing track
|
||||||
|
- `!help`: Shows available commands
|
||||||
|
|
||||||
|
Admin commands:
|
||||||
|
- `!start`: Start stream monitoring
|
||||||
|
- `!stop`: Stop stream monitoring
|
||||||
|
- `!reconnect`: Reconnect to stream (with status feedback)
|
||||||
|
- `!restart`: Restart the bot (requires using bot.sh)
|
||||||
|
- `!quit`: Shutdown the bot
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
The bot logs important events to stdout with timestamps. Log level is set to INFO by default.
|
The bot supports different logging levels configurable in the config.yaml:
|
||||||
|
- DEBUG: Detailed information for troubleshooting
|
||||||
|
- INFO: General operational messages (default)
|
||||||
|
- WARNING: Warning messages and potential issues
|
||||||
|
- ERROR: Error messages only
|
||||||
|
- CRITICAL: Critical failures only
|
||||||
|
|
||||||
|
Logs include:
|
||||||
|
- Stream health status
|
||||||
|
- Command processing
|
||||||
|
- Connection status
|
||||||
|
- Error details
|
||||||
|
|
||||||
|
The bot also maintains an ERROR.log file for critical issues.
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
- Automatically reconnects on connection drops
|
- Automatically reconnects on connection drops
|
||||||
- Retries stream monitoring on errors
|
- Retries stream monitoring on errors
|
||||||
- Logs errors for debugging
|
- Smart metadata URL resolution
|
||||||
|
- Connection status verification and reporting
|
||||||
- Health checks every 5 minutes (configurable)
|
- Health checks every 5 minutes (configurable)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
24
bot.sh
Executable file
24
bot.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if any arguments were provided
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Usage: $0 <config.yaml> [additional configs...]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
# Remove any existing restart flags before starting
|
||||||
|
rm -f .restart_flag_*
|
||||||
|
|
||||||
|
python main.py "$@"
|
||||||
|
|
||||||
|
# Check for any restart flags
|
||||||
|
if ls .restart_flag_* 1> /dev/null 2>&1; then
|
||||||
|
echo "Restart flag(s) found, restarting bot(s)..."
|
||||||
|
sleep 1
|
||||||
|
continue
|
||||||
|
else
|
||||||
|
echo "Bot(s) exited without restart flag, stopping..."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -1,19 +1,39 @@
|
|||||||
irc:
|
irc:
|
||||||
host: "irc.someircserver.net"
|
host: "irc.libera.chat"
|
||||||
port: 6667 # asif does not support ssl as of 2025-02-23
|
port: 6667
|
||||||
nick: "bot"
|
nick: "IcecastBot"
|
||||||
user: "bot"
|
user: "icecastbot"
|
||||||
realname: "Botty Bot - https://bot.site"
|
realname: "Icecast IRC Bot"
|
||||||
channel: "#channel"
|
channel: "#yourchannel"
|
||||||
|
|
||||||
stream:
|
stream:
|
||||||
url: "http://your-icecast-server.com:8000/" # this is the *base* url of the icecast server
|
url: "https://your.stream.url" # Base URL without /stream or .mp3
|
||||||
endpoint: "stream" # this is the endpoint of the icecast server (e.g. /stream or .mp3)
|
endpoint: "/stream" # The endpoint part (e.g. /stream, /radio.mp3)
|
||||||
health_check_interval: 300
|
health_check_interval: 300 # How often to log health status (in seconds)
|
||||||
|
|
||||||
announce:
|
announce:
|
||||||
format: "\x02Now playing:\x02 {song}"
|
format: "\x02Now playing:\x02 {song}" # Format for song announcements
|
||||||
ignore_patterns:
|
ignore_patterns: # Don't announce songs matching these patterns
|
||||||
- "Unknown"
|
- "Unknown"
|
||||||
- "Unable to fetch metadata"
|
- "Unable to fetch metadata"
|
||||||
# Add more patterns to ignore here
|
- "Error fetching metadata"
|
||||||
|
|
||||||
|
commands:
|
||||||
|
prefix: "!" # Command prefix (e.g. !np, !help)
|
||||||
|
require_nick_prefix: false # If true, commands must be prefixed with "botname: " or "botname, "
|
||||||
|
allow_private_commands: false # If true, allows commands in private messages
|
||||||
|
|
||||||
|
help: # Help message templates
|
||||||
|
specific_format: "\x02{prefix}{cmd}\x02: {desc}" # Format for specific command help
|
||||||
|
list_format: "(\x02{cmd}\x02, {desc})" # Format for commands in list
|
||||||
|
list_separator: " | " # Separator between commands in list
|
||||||
|
|
||||||
|
admin:
|
||||||
|
users: # List of users who can use admin commands (use "*" for anyone)
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: "INFO" # Logging level: DEBUG, INFO, WARNING, ERROR, or CRITICAL
|
||||||
|
format: "%(asctime)s - %(levelname)s - %(message)s" # Format for console logs
|
||||||
|
error_format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" # Format for error logs
|
||||||
|
datefmt: "%H:%M:%S" # Date/time format for log timestamps
|
||||||
99
generate_docs.py
Executable file
99
generate_docs.py
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from main import IcecastBot
|
||||||
|
|
||||||
|
def get_version():
|
||||||
|
"""Get the current version from VERSION file."""
|
||||||
|
try:
|
||||||
|
with open('VERSION') as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
def format_docstring(obj):
|
||||||
|
"""Format a docstring into markdown."""
|
||||||
|
doc = inspect.getdoc(obj)
|
||||||
|
if not doc:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Split into description and args
|
||||||
|
parts = doc.split('\n\n')
|
||||||
|
formatted = [parts[0]] # Description
|
||||||
|
|
||||||
|
for part in parts[1:]:
|
||||||
|
if part.startswith('Args:'):
|
||||||
|
formatted.append("\n**Arguments:**\n")
|
||||||
|
# Parse args section
|
||||||
|
args = part.replace('Args:', '').strip().split('\n')
|
||||||
|
for arg in args:
|
||||||
|
if ':' in arg:
|
||||||
|
name, desc = arg.split(':', 1)
|
||||||
|
formatted.append(f"- `{name.strip()}`: {desc.strip()}")
|
||||||
|
elif part.startswith('Returns:'):
|
||||||
|
formatted.append("\n**Returns:**\n")
|
||||||
|
formatted.append(part.replace('Returns:', '').strip())
|
||||||
|
|
||||||
|
return '\n'.join(formatted)
|
||||||
|
|
||||||
|
def generate_command_docs(config_path='config.yaml.example'):
|
||||||
|
"""Generate command documentation from help templates."""
|
||||||
|
try:
|
||||||
|
with open(config_path) as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return "No command documentation available (config file not found)"
|
||||||
|
|
||||||
|
help_config = config.get('commands', {}).get('help', {})
|
||||||
|
if not help_config:
|
||||||
|
return "No command documentation available (help template not found)"
|
||||||
|
|
||||||
|
docs = ["## Commands\n"]
|
||||||
|
|
||||||
|
# Regular commands
|
||||||
|
if 'commands' in help_config.get('sections', {}):
|
||||||
|
docs.append("### Regular Commands\n")
|
||||||
|
for cmd, desc in help_config['sections']['commands']['commands'].items():
|
||||||
|
docs.append(f"- `{cmd}`: {desc}")
|
||||||
|
docs.append("")
|
||||||
|
|
||||||
|
# Admin commands
|
||||||
|
if 'admin' in help_config.get('sections', {}):
|
||||||
|
docs.append("### Admin Commands\n")
|
||||||
|
docs.append("These commands are only available to users listed in the `admin.users` config section.\n")
|
||||||
|
for cmd, desc in help_config['sections']['admin']['commands'].items():
|
||||||
|
docs.append(f"- `{cmd}`: {desc}")
|
||||||
|
docs.append("")
|
||||||
|
|
||||||
|
return '\n'.join(docs)
|
||||||
|
|
||||||
|
def generate_docs():
|
||||||
|
"""Generate full documentation in markdown format."""
|
||||||
|
version = get_version()
|
||||||
|
|
||||||
|
docs = [
|
||||||
|
f"# Icecast IRC Bot Documentation v{version}\n",
|
||||||
|
"This document is automatically generated from the codebase.\n",
|
||||||
|
"## Overview\n",
|
||||||
|
format_docstring(IcecastBot),
|
||||||
|
"\n## Configuration\n",
|
||||||
|
"See `config.yaml.example` for a full example configuration file.\n",
|
||||||
|
generate_command_docs(),
|
||||||
|
"\n## Methods\n"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Document public methods
|
||||||
|
for name, method in inspect.getmembers(IcecastBot, predicate=inspect.isfunction):
|
||||||
|
if not name.startswith('_'): # Only public methods
|
||||||
|
docs.append(f"### {name}\n")
|
||||||
|
docs.append(format_docstring(method))
|
||||||
|
docs.append("\n")
|
||||||
|
|
||||||
|
# Write to DOCS.md
|
||||||
|
with open('DOCS.md', 'w') as f:
|
||||||
|
f.write('\n'.join(docs))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
generate_docs()
|
||||||
194
icecast-irc-bot-manager.py
Normal file
194
icecast-irc-bot-manager.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger('icecast-irc-bot-manager')
|
||||||
|
|
||||||
|
class BotManager:
|
||||||
|
"""Manages multiple Icecast IRC bot instances."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.bots: Dict[str, asyncio.subprocess.Process] = {}
|
||||||
|
self.config_dir = Path('/etc/icecast-irc-bot')
|
||||||
|
self.socket_dir = Path(tempfile.gettempdir())
|
||||||
|
|
||||||
|
async def start_bot(self, config_path: Path) -> bool:
|
||||||
|
"""Start a bot instance with the given config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: Path to the bot's config file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if bot was started successfully
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Create unique name for this bot instance
|
||||||
|
bot_id = config_path.stem
|
||||||
|
|
||||||
|
# Check if bot is already running
|
||||||
|
if bot_id in self.bots:
|
||||||
|
logger.warning(f"Bot {bot_id} is already running")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Start the bot process
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
sys.executable, '-m', 'main',
|
||||||
|
'--config', str(config_path),
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
self.bots[bot_id] = process
|
||||||
|
logger.info(f"Started bot {bot_id} (PID: {process.pid})")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start bot with config {config_path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def stop_bot(self, bot_id: str) -> bool:
|
||||||
|
"""Stop a running bot instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_id: ID of the bot to stop
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if bot was stopped successfully
|
||||||
|
"""
|
||||||
|
if bot_id not in self.bots:
|
||||||
|
logger.warning(f"Bot {bot_id} is not running")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
process = self.bots[bot_id]
|
||||||
|
process.terminate()
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(process.wait(), timeout=5.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
process.kill()
|
||||||
|
await process.wait()
|
||||||
|
|
||||||
|
del self.bots[bot_id]
|
||||||
|
logger.info(f"Stopped bot {bot_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to stop bot {bot_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def restart_bot(self, bot_id: str) -> bool:
|
||||||
|
"""Restart a running bot instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_id: ID of the bot to restart
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if bot was restarted successfully
|
||||||
|
"""
|
||||||
|
# Find the config file for this bot
|
||||||
|
config_path = self.config_dir / f"{bot_id}.yaml"
|
||||||
|
if not config_path.exists():
|
||||||
|
logger.error(f"Config file not found for bot {bot_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Stop the bot if it's running
|
||||||
|
if bot_id in self.bots:
|
||||||
|
if not await self.stop_bot(bot_id):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Start the bot with the same config
|
||||||
|
return await self.start_bot(config_path)
|
||||||
|
|
||||||
|
async def list_bots(self) -> List[Dict]:
|
||||||
|
"""List all running bot instances.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict]: List of bot info dictionaries
|
||||||
|
"""
|
||||||
|
bot_info = []
|
||||||
|
for bot_id, process in self.bots.items():
|
||||||
|
info = {
|
||||||
|
'id': bot_id,
|
||||||
|
'pid': process.pid,
|
||||||
|
'running': process.returncode is None
|
||||||
|
}
|
||||||
|
bot_info.append(info)
|
||||||
|
return bot_info
|
||||||
|
|
||||||
|
async def cleanup(self):
|
||||||
|
"""Clean up all running bots."""
|
||||||
|
for bot_id in list(self.bots.keys()):
|
||||||
|
await self.stop_bot(bot_id)
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
manager = BotManager()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.command == 'list':
|
||||||
|
bot_info = await manager.list_bots()
|
||||||
|
if bot_info:
|
||||||
|
print(json.dumps(bot_info, indent=2))
|
||||||
|
else:
|
||||||
|
print("No bots running")
|
||||||
|
|
||||||
|
elif args.command == 'start':
|
||||||
|
if not args.config:
|
||||||
|
print("Error: --config required for start command")
|
||||||
|
sys.exit(1)
|
||||||
|
config_path = Path(args.config)
|
||||||
|
if config_path.is_dir():
|
||||||
|
# Start all bots in directory
|
||||||
|
for config_file in config_path.glob('*.yaml'):
|
||||||
|
await manager.start_bot(config_file)
|
||||||
|
else:
|
||||||
|
# Start single bot
|
||||||
|
await manager.start_bot(config_path)
|
||||||
|
|
||||||
|
elif args.command == 'stop':
|
||||||
|
if not args.bot_id:
|
||||||
|
print("Error: bot_id required for stop command")
|
||||||
|
sys.exit(1)
|
||||||
|
await manager.stop_bot(args.bot_id)
|
||||||
|
|
||||||
|
elif args.command == 'restart':
|
||||||
|
if not args.bot_id:
|
||||||
|
print("Error: bot_id required for restart command")
|
||||||
|
sys.exit(1)
|
||||||
|
await manager.restart_bot(args.bot_id)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Shutting down...")
|
||||||
|
await manager.cleanup()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unhandled error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Set up signal handlers
|
||||||
|
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||||
|
signal.signal(sig, lambda s, f: sys.exit(0))
|
||||||
|
|
||||||
|
# Run the manager
|
||||||
|
asyncio.run(main())
|
||||||
26
icecast-irc-bot-manager.service
Normal file
26
icecast-irc-bot-manager.service
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Icecast IRC Bot Manager
|
||||||
|
After=network.target
|
||||||
|
Wants=network.target
|
||||||
|
Documentation=https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=icecast-bot
|
||||||
|
Group=icecast-bot
|
||||||
|
Environment=VIRTUAL_ENV=/opt/icecast-irc-bot/venv
|
||||||
|
Environment=PATH=/opt/icecast-irc-bot/venv/bin:$PATH
|
||||||
|
ExecStart=/opt/icecast-irc-bot/venv/bin/python3 /usr/local/bin/icecast-irc-bot-manager --config /etc/icecast-irc-bot/config.yaml
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
StandardOutput=append:/var/log/icecast-irc-bot/bot.log
|
||||||
|
StandardError=append:/var/log/icecast-irc-bot/error.log
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
71
install.sh
Executable file
71
install.sh
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Exit on any error
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "Please run as root"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create icecast-bot user and group if they don't exist
|
||||||
|
if ! getent group icecast-bot >/dev/null; then
|
||||||
|
groupadd icecast-bot
|
||||||
|
fi
|
||||||
|
if ! getent passwd icecast-bot >/dev/null; then
|
||||||
|
useradd -r -g icecast-bot -s /bin/false icecast-bot
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
mkdir -p /etc/icecast-irc-bot
|
||||||
|
mkdir -p /var/log/icecast-irc-bot
|
||||||
|
mkdir -p /opt/icecast-irc-bot
|
||||||
|
|
||||||
|
# Set ownership
|
||||||
|
chown icecast-bot:icecast-bot /etc/icecast-irc-bot
|
||||||
|
chown icecast-bot:icecast-bot /var/log/icecast-irc-bot
|
||||||
|
chown icecast-bot:icecast-bot /opt/icecast-irc-bot
|
||||||
|
|
||||||
|
# Create and activate virtual environment
|
||||||
|
python3 -m venv /opt/icecast-irc-bot/venv
|
||||||
|
export VIRTUAL_ENV="/opt/icecast-irc-bot/venv"
|
||||||
|
export PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
unset PYTHONHOME
|
||||||
|
|
||||||
|
# Install Python dependencies in virtual environment
|
||||||
|
pip3 install --upgrade pip
|
||||||
|
pip3 install hatchling
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
pip3 install .
|
||||||
|
|
||||||
|
# Install manager script
|
||||||
|
cp icecast-irc-bot-manager.py /usr/local/bin/icecast-irc-bot-manager
|
||||||
|
chmod +x /usr/local/bin/icecast-irc-bot-manager
|
||||||
|
|
||||||
|
# Update manager script shebang to use venv Python
|
||||||
|
sed -i "1c#\!$VIRTUAL_ENV/bin/python3" /usr/local/bin/icecast-irc-bot-manager
|
||||||
|
|
||||||
|
# Install service file
|
||||||
|
cp icecast-irc-bot-manager.service /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Copy example config if it doesn't exist
|
||||||
|
if [ ! -f /etc/icecast-irc-bot/config.yaml ]; then
|
||||||
|
cp config.yaml.example /etc/icecast-irc-bot/config.yaml
|
||||||
|
chown icecast-bot:icecast-bot /etc/icecast-irc-bot/config.yaml
|
||||||
|
chmod 640 /etc/icecast-irc-bot/config.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installation complete!"
|
||||||
|
echo
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1. Configure your bot(s) in /etc/icecast-irc-bot/config.yaml"
|
||||||
|
echo "2. Start the service: systemctl start icecast-irc-bot-manager"
|
||||||
|
echo "3. Enable at boot: systemctl enable icecast-irc-bot-manager"
|
||||||
|
echo
|
||||||
|
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"
|
||||||
31
pyproject.toml
Normal file
31
pyproject.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[project]
|
||||||
|
name = "icecast-irc-bot"
|
||||||
|
dynamic = ["version"]
|
||||||
|
description = "Icecast metadata IRC announcer bot"
|
||||||
|
authors = [
|
||||||
|
{name = "cottongin", email = "cottongin@cottongin.xyz"},
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"asif",
|
||||||
|
"aiohttp",
|
||||||
|
"pyyaml",
|
||||||
|
]
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["."]
|
||||||
|
|
||||||
|
[tool.hatch.build]
|
||||||
|
include = [
|
||||||
|
"*.py",
|
||||||
|
"README*",
|
||||||
|
"LICENSE*",
|
||||||
|
"config.yaml.example"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {file = "VERSION"}
|
||||||
Reference in New Issue
Block a user