diff --git a/bot.py b/radio_bot.py similarity index 83% rename from bot.py rename to radio_bot.py index b5c5839..357fd9d 100644 --- a/bot.py +++ b/radio_bot.py @@ -1,11 +1,4 @@ #!/usr/bin/env python3 -""" -Main Application Entry Point. - -This module initializes the XMPP bot, loads configuration, manages plugins -(OMEMO, MUC, Jingle), and orchestrates the interaction between the audio -streamer, playlist manager, and XMPP interface. -""" import os import sys @@ -59,9 +52,7 @@ NS_JINGLE = "urn:xmpp:jingle:1" NS_JINGLE_MSG = "urn:xmpp:jingle-message:0" NS_HINTS = "urn:xmpp:hints" - class LRUCache(OrderedDict): - """Simple LRU Cache implementation for storing search results.""" def __init__(self, maxsize=25): super().__init__() self.maxsize = maxsize @@ -85,10 +76,8 @@ class LRUCache(OrderedDict): oldest = next(iter(self)) del self[oldest] - if OMEMO_AVAILABLE: class OMEMOStorageImpl(Storage): - """File-based storage backend for OMEMO keys.""" def __init__(self, json_file_path: Path) -> None: super().__init__() @@ -117,7 +106,6 @@ if OMEMO_AVAILABLE: class XEP_0384Impl(XEP_0384): - """OMEMO Plugin Configuration.""" default_config = { "fallback_message": "This message is OMEMO encrypted.", @@ -164,10 +152,8 @@ if OMEMO_AVAILABLE: register_plugin(XEP_0384Impl) - @dataclass(slots=True) class MessageContext: - """Encapsulates context about an incoming message for command handling.""" sender: JID sender_bare: str mtype: str @@ -176,7 +162,6 @@ class MessageContext: room_jid: Optional[str] = None nick: Optional[str] = None - @dataclass class BotConfig: jid: str @@ -210,7 +195,6 @@ class BotConfig: omemo_enabled: bool omemo_store_path: str - def load_config(config_file: str = "config.ini") -> BotConfig: if not os.path.exists(config_file): raise FileNotFoundError(f"Config file not found: {config_file}") @@ -256,18 +240,7 @@ def load_config(config_file: str = "config.ini") -> BotConfig: omemo_store_path=c.get('OMEMO', 'store_path', fallback='radio_omemo_store.json') ) - class RadioBot(slixmpp.ClientXMPP): - """ - Core XMPP Bot logic. - - Handles: - - Connection and Authentication - - OMEMO and Plaintext message routing - - Command processing - - Integration with PlaylistManager, AudioStreamer, and JingleWebRTCHandler - - Task management and memory monitoring - """ MEMORY_WARNING_THRESHOLD = 400 MEMORY_CRITICAL_THRESHOLD = 600 @@ -303,9 +276,9 @@ class RadioBot(slixmpp.ClientXMPP): send_transport_info_callback=self._send_transport_info, on_track_end=self.on_track_end ) - logger.info("Jingle enabled with WebRTC") + logger.info("โœ… Jingle enabled with WebRTC") elif config.jingle_enabled: - logger.warning("Jingle enabled but 'aiortc' missing") + logger.warning("โš ๏ธ Jingle enabled but 'aiortc' missing") self.current_station = None self.is_playing_station = False @@ -316,19 +289,17 @@ class RadioBot(slixmpp.ClientXMPP): self._is_transitioning = False - # Rapid skip detection variables self._skip_count = 0 self._skip_window_start = 0 - # Task management set to allow proper cleanup on shutdown self._managed_tasks: Set[asyncio.Task] = set() self._gc_task: Optional[asyncio.Task] = None self._memory_monitor_task: Optional[asyncio.Task] = None - self.register_plugin('xep_0030') # Service Discovery - self.register_plugin('xep_0045') # MUC - self.register_plugin('xep_0115') # Entity Capabilities - self.register_plugin('xep_0199') # Ping + self.register_plugin('xep_0030') + self.register_plugin('xep_0045') + self.register_plugin('xep_0115') + self.register_plugin('xep_0199') if self.omemo_active: self.register_plugin('xep_0085') @@ -339,9 +310,9 @@ class RadioBot(slixmpp.ClientXMPP): {"json_file_path": config.omemo_store_path}, module=sys.modules[__name__] ) - logger.info("OMEMO encryption enabled") + logger.info("๐Ÿ”’ OMEMO encryption enabled") elif config.omemo_enabled: - logger.warning("OMEMO enabled but libraries missing") + logger.warning("โš ๏ธ OMEMO enabled but libraries missing") if self.jingle: for f in [NS_JINGLE, NS_JINGLE_MSG, @@ -366,7 +337,6 @@ class RadioBot(slixmpp.ClientXMPP): self.add_event_handler("message", self._handle_plain_message) self.add_event_handler("groupchat_message", self._handle_plain_muc_message) - # Jingle Message Initiation (XEP-0353) self.register_handler(Callback( 'JinglePropose', MatchXPath(f'{{jabber:client}}message/{{{NS_JINGLE_MSG}}}propose'), @@ -379,7 +349,6 @@ class RadioBot(slixmpp.ClientXMPP): self._handle_jingle_retract )) - # Standard Jingle IQs self.register_handler(Callback( 'JingleIQ', MatchXPath(f'{{jabber:client}}iq/{{{NS_JINGLE}}}jingle'), @@ -401,6 +370,10 @@ class RadioBot(slixmpp.ClientXMPP): 'list': self.cmd_list, 'shuffle': self.cmd_shuffle, 'search': self.cmd_search, + 'sc': self.cmd_sc, + 'bc': self.cmd_bc, + 'lib': self.cmd_library, + 'local': self.cmd_library, 'dl': self.cmd_dl, 'scan': self.cmd_scan, 'url': self.cmd_url, @@ -418,41 +391,38 @@ class RadioBot(slixmpp.ClientXMPP): } def _create_task(self, coro) -> asyncio.Task: - """Helper to create asyncio tasks and track them for cleanup.""" task = asyncio.create_task(coro) self._managed_tasks.add(task) task.add_done_callback(lambda t: self._managed_tasks.discard(t)) return task def _cleanup_done_tasks(self): - """Removes completed tasks from the tracking set.""" done_tasks = {t for t in self._managed_tasks if t.done()} self._managed_tasks -= done_tasks return len(done_tasks) async def on_start(self, event): - """Called when XMPP session is established.""" - logger.info("Bot Session Started") + logger.info("๐Ÿš€ Bot Session Started") self.send_presence() await self.get_roster() if self.streamer: await self.streamer.start() - logger.info(f"HTTP Stream: {self.streamer.stream_url}") + logger.info(f"๐Ÿ“ก HTTP Stream: {self.streamer.stream_url}") if self.jingle: await self.jingle.start_cleanup_task() count = await self.playlist_manager.scan_library() self.playlist_manager.add_library_to_playlist() - logger.info(f"Library loaded: {count} tracks") + logger.info(f"๐ŸŽต Library loaded: {count} tracks") for room, nick in [(self.config.radio_room, self.config.radio_room_nick), (self.config.management_room, self.config.management_room_nick)]: if room: try: await self.plugin['xep_0045'].join_muc(room, nick) - logger.info(f"Joined MUC: {room}") + logger.info(f"๐Ÿ“ข Joined MUC: {room}") except Exception as e: logger.error(f"MUC Join Failed ({room}): {e}") @@ -465,26 +435,20 @@ class RadioBot(slixmpp.ClientXMPP): self._memory_monitor_task = asyncio.create_task(self._memory_monitor()) async def _periodic_gc(self): - """Aggressive garbage collection to handle heavy media object churn.""" while True: await asyncio.sleep(120) try: self._search_results.clear_old(keep=10) - cleaned = self._cleanup_done_tasks() - gc.collect(generation=0) gc.collect(generation=1) gc.collect(generation=2) - - logger.debug(f"GC completed, cleaned {cleaned} tasks, remaining: {len(self._managed_tasks)}") - + logger.debug(f"๐Ÿงน GC completed, cleaned {cleaned} tasks, remaining: {len(self._managed_tasks)}") except Exception as e: logger.error(f"GC error: {e}") async def _memory_monitor(self): - """Monitors RSS memory usage and triggers emergency cleanup if critical.""" try: import resource has_resource = True @@ -498,32 +462,26 @@ class RadioBot(slixmpp.ClientXMPP): try: if has_resource: usage = resource.getrusage(resource.RUSAGE_SELF) - # Convert to MB (Linux returns KB, macOS returns bytes) mem_mb = usage.ru_maxrss / 1024 if sys.platform == 'darwin': mem_mb = usage.ru_maxrss / (1024 * 1024) - logger.info(f"Memory usage: {mem_mb:.1f} MB, Tasks: {len(self._managed_tasks)}") + logger.info(f"๐Ÿ“Š Memory usage: {mem_mb:.1f} MB, Tasks: {len(self._managed_tasks)}") if mem_mb > self.MEMORY_WARNING_THRESHOLD: - logger.warning(f"High memory usage ({mem_mb:.1f} MB)") - + logger.warning(f"โš ๏ธ High memory usage ({mem_mb:.1f} MB)") self._search_results.clear() self._cleanup_done_tasks() - gc.collect() gc.collect() gc.collect() if mem_mb > self.MEMORY_CRITICAL_THRESHOLD: - logger.error(f"Critical memory usage ({mem_mb:.1f} MB), aggressive cleanup") - - # Terminate oldest calls to save memory + logger.error(f"๐Ÿšจ Critical memory usage ({mem_mb:.1f} MB), aggressive cleanup") if self.jingle and len(self.jingle.sessions) > 5: oldest_sessions = list(self.jingle.sessions.keys())[:-5] for sid in oldest_sessions: await self.jingle.stop_session(sid) - gc.collect() except Exception as e: @@ -541,7 +499,6 @@ class RadioBot(slixmpp.ClientXMPP): return 0.0 async def reply(self, ctx: MessageContext, text: str) -> None: - """Sends a reply matching the context (MUC vs Direct, OMEMO vs Plain).""" if ctx.is_muc: msg = self.make_message(mto=ctx.room_jid, mtype='groupchat') msg["body"] = text @@ -578,7 +535,6 @@ class RadioBot(slixmpp.ClientXMPP): await self._send_plain(mto, mtype, text) async def _handle_direct_message(self, stanza: Message) -> None: - """Handles OMEMO-aware direct messages.""" try: mfrom = stanza["from"] mtype = stanza["type"] @@ -589,7 +545,6 @@ class RadioBot(slixmpp.ClientXMPP): if mfrom.bare == self.boundjid.bare: return - # Ignore Jingle signal messages treated as chat if stanza.xml.find(f'{{{NS_JINGLE_MSG}}}propose') is not None: return if stanza.xml.find(f'{{{NS_JINGLE_MSG}}}proceed') is not None: @@ -615,7 +570,7 @@ class RadioBot(slixmpp.ClientXMPP): sender=mfrom, sender_bare=str(mfrom.bare), mtype=mtype, is_encrypted=True, is_muc=False ) - await self.reply(ctx, f"Could not decrypt: {e}") + await self.reply(ctx, f"โŒ Could not decrypt: {e}") return else: if stanza["body"]: @@ -709,17 +664,16 @@ class RadioBot(slixmpp.ClientXMPP): if body.startswith('!'): await self._run_command(ctx, body[1:]) elif body.lower() in ('radio', 'help'): - await self.reply(ctx, "XMPP Radio Bot\nType !help for commands.\nCall me to listen!") + await self.reply(ctx, "๐Ÿ“ป XMPP Radio Bot\nType !help for commands.\nCall me to listen!") async def _run_command(self, ctx: MessageContext, cmdline: str): - """Parses and executes bot commands.""" parts = cmdline.split(None, 1) cmd = parts[0].lower() arg = parts[1] if len(parts) > 1 else "" needs_auth = {'stop', 'next', 'station', 'dl', 'scan', 'hangup', 'clearq'} if cmd in needs_auth and not self._check_access(ctx.sender_bare): - await self.reply(ctx, "Permission Denied") + await self.reply(ctx, "โ›” Permission Denied") return if cmd in self.commands: @@ -727,10 +681,9 @@ class RadioBot(slixmpp.ClientXMPP): await self.commands[cmd](ctx, arg) except Exception as e: logger.error(f"Command error: {e}", exc_info=True) - await self.reply(ctx, f"Error: {e}") + await self.reply(ctx, f"โŒ Error: {e}") async def _send_transport_info(self, session: JingleSession, candidate: Dict): - """Sends an ICE candidate to the peer via Jingle transport-info.""" iq = self.make_iq_set(ito=session.peer_jid) jingle = ET.Element('jingle', { @@ -769,17 +722,16 @@ class RadioBot(slixmpp.ClientXMPP): try: iq.send() - logger.debug(f"Sent transport-info: {candidate.get('type')} {candidate.get('ip')}") + logger.debug(f"๐Ÿ“ค Sent transport-info: {candidate.get('type')} {candidate.get('ip')}") except Exception as e: logger.error(f"Failed to send transport-info: {e}") def _handle_jingle_propose(self, msg): - """Handles incoming XEP-0353 Jingle Message initiation.""" sender = str(msg['from']) propose = msg.xml.find(f'{{{NS_JINGLE_MSG}}}propose') sid = propose.get('id') - logger.info(f"Call from {sender}, ID={sid}") + logger.info(f"๐Ÿ“ž Call from {sender}, ID={sid}") self._send_proceed(sender, sid) if self.jingle: @@ -788,7 +740,7 @@ class RadioBot(slixmpp.ClientXMPP): def _handle_jingle_retract(self, msg): retract = msg.xml.find(f'{{{NS_JINGLE_MSG}}}retract') sid = retract.get('id') - logger.info(f"Call Retracted: {sid}") + logger.info(f"๐Ÿ”ด Call Retracted: {sid}") if self.jingle: self._create_task(self.jingle.stop_session(sid)) @@ -798,7 +750,7 @@ class RadioBot(slixmpp.ClientXMPP): msg.xml.append(proceed) msg.xml.append(ET.Element(f'{{{NS_HINTS}}}store')) msg.send() - logger.info(f"Sent PROCEED to {to_jid}") + logger.info(f"๐Ÿ“ค Sent PROCEED to {to_jid}") def _handle_jingle_iq(self, iq): if not self.jingle: @@ -815,7 +767,7 @@ class RadioBot(slixmpp.ClientXMPP): sid = jingle.get('sid') peer = str(iq['from']) - logger.info(f"Jingle IQ: {action} (sid={sid})") + logger.info(f"๐Ÿ“จ Jingle IQ: {action} (sid={sid})") try: if action == 'session-initiate': @@ -830,7 +782,7 @@ class RadioBot(slixmpp.ClientXMPP): accept_iq = self.make_iq_set(ito=peer) accept_iq.xml.append(ET.fromstring(accept_xml)) accept_iq.send() - logger.info("Sent session-accept") + logger.info("โœ… Sent session-accept") elif action == 'transport-info': session = self.jingle.get_session(sid) @@ -843,7 +795,7 @@ class RadioBot(slixmpp.ClientXMPP): iq.reply().send() elif action == 'session-terminate': - logger.info(f"Peer hung up: {sid}") + logger.info(f"๐Ÿ”ด Peer hung up: {sid}") await self.jingle.stop_session(sid) iq.reply().send() @@ -866,12 +818,11 @@ class RadioBot(slixmpp.ClientXMPP): return jid in self.config.allowed_jids async def _play_track(self, track: Track): - """Starts playback for both HTTP and Jingle subsystems.""" async with self._track_transition_lock: self.is_playing_station = False self.current_station = None - logger.info(f"Playing track: {track.display_name}") + logger.info(f"๐ŸŽต Playing track: {track.display_name}") try: if self.streamer: @@ -882,7 +833,6 @@ class RadioBot(slixmpp.ClientXMPP): ) if self.jingle: - # Slight delay ensures files handle gets released if necessary await asyncio.sleep(0.1) self.jingle.set_audio_source(track.path, force_restart=True) @@ -892,17 +842,15 @@ class RadioBot(slixmpp.ClientXMPP): gc.collect(generation=0) async def on_track_end(self): - """Callback for when a track finishes. Handles auto-advance logic.""" current_time = time.time() - # Detect rapid skipping loop (usually corrupt files) if current_time - self._skip_window_start > 10.0: self._skip_count = 0 self._skip_window_start = current_time self._skip_count += 1 if self._skip_count > 5: - logger.error("Detected rapid skipping loop. Stopping playback.") + logger.error("๐Ÿšจ Detected rapid skipping loop. Stopping playback.") await self.cmd_stop(None, "force") return @@ -922,13 +870,13 @@ class RadioBot(slixmpp.ClientXMPP): logger.debug("Playing station, not auto-transitioning") return - logger.info("Track ended, getting next track...") + logger.info("๐Ÿ”„ Track ended, getting next track...") t = self.playlist_manager.next_track() if t: await asyncio.sleep(0.5) await self._play_track(t) - logger.info(f"Auto-playing: {t.display_name}") + logger.info(f"โ–ถ๏ธ Auto-playing: {t.display_name}") else: logger.info("No more tracks in queue/playlist") @@ -946,7 +894,7 @@ class RadioBot(slixmpp.ClientXMPP): def _get_np_text(self): if self.is_playing_station: - return f"Station: {self.current_station}" + return f"๐Ÿ“ป {self.current_station}" t = self.playlist_manager.get_current_track() if t: @@ -959,13 +907,13 @@ class RadioBot(slixmpp.ClientXMPP): async def cmd_help(self, ctx: MessageContext, arg: str): help_text = ( - "Radio Bot Commands:\n\n" - "Playback:\n" + "๐ŸŽต **Radio Bot Commands:**\n\n" + "**Playback:**\n" "!play [query] - Play/Search music\n" "!stop / !next / !prev - Control\n" "!np - Now playing\n" "!shuffle - Shuffle playlist\n\n" - "Queue:\n" + "**Queue:**\n" "!queue (!q) - View queue\n" "!addq - Add to queue\n" "!playnext - Play next\n" @@ -973,21 +921,24 @@ class RadioBot(slixmpp.ClientXMPP): "!removeq <#> - Remove from queue\n" "!shuffleq - Shuffle queue\n" "!history - Recent tracks\n\n" - "Download:\n" + "**Search & Download:**\n" "!search - Search YouTube\n" + "!sc - Search SoundCloud\n" + "!bc - Search Bandcamp\n" + "!lib - Search Local Library\n" "!dl - Download\n\n" - "Radio:\n" + "**Radio:**\n" "!station - Play station\n" "!stations - List stations\n" "!list [page] - Show playlist\n" "!scan - Rescan library\n" "!callers - Active calls\n\n" - "Unnecessary commands DO NOT USE:\n" + "**Unnecessary commands DO NOT USE:**\n" "!url - Returns a local URL so nobody needs it\n" "!mem - Shows memory usage but it crashes anyways\n" "!hangup - Just use your clients disconnect button\n\n" - "Version Info and About:\n" - "XMPP Radio Bot v0.1 (codename: Absolute Solver)" + "**Version Info and About:**\n" + "XMPP Radio Bot v0.2 (codename: Branded Pens)" ) await self.reply(ctx, help_text) @@ -996,14 +947,14 @@ class RadioBot(slixmpp.ClientXMPP): res = self.playlist_manager.search_library(arg) if res: await self._play_track(res[0]) - await self.reply(ctx, f"Playing: {res[0].display_name}") + await self.reply(ctx, f"โ–ถ๏ธ {res[0].display_name}") else: - await self.reply(ctx, "Not found in library") + await self.reply(ctx, "โŒ Not found in library") else: t = self.playlist_manager.next_track() if t: await self._play_track(t) - await self.reply(ctx, f"Playing: {t.display_name}") + await self.reply(ctx, f"โ–ถ๏ธ {t.display_name}") async def cmd_stop(self, ctx: MessageContext, arg: str): if self.streamer: @@ -1016,34 +967,34 @@ class RadioBot(slixmpp.ClientXMPP): self.is_playing_station = False self.current_station = None - await self.reply(ctx, "Stopped") + await self.reply(ctx, "โน๏ธ Stopped") async def cmd_next(self, ctx: MessageContext, arg: str): t = self.playlist_manager.next_track() if t: await self._play_track(t) - await self.reply(ctx, f"Next: {t.display_name}") + await self.reply(ctx, f"โญ๏ธ {t.display_name}") else: - await self.reply(ctx, "No next track") + await self.reply(ctx, "โŒ No next track") async def cmd_prev(self, ctx: MessageContext, arg: str): t = self.playlist_manager.previous_track() if t: await self._play_track(t) - await self.reply(ctx, f"Previous: {t.display_name}") + await self.reply(ctx, f"โฎ๏ธ {t.display_name}") else: - await self.reply(ctx, "No previous track") + await self.reply(ctx, "โŒ No previous track") async def cmd_np(self, ctx: MessageContext, arg: str): np_text = self._get_np_text() queue_size = self.playlist_manager.queue.size - queue_info = f"\nQueue: {queue_size} tracks" if queue_size > 0 else "" - await self.reply(ctx, f"Now Playing: {np_text}{queue_info}") + queue_info = f"\n๐Ÿ“‹ Queue: {queue_size} tracks" if queue_size > 0 else "" + await self.reply(ctx, f"๐ŸŽต Now Playing: {np_text}{queue_info}") async def cmd_station(self, ctx: MessageContext, arg: str): if not arg or arg not in self.config.stations: stations = ', '.join(self.config.stations.keys()) or "None configured" - await self.reply(ctx, f"Stations: {stations}") + await self.reply(ctx, f"๐Ÿ“ป Stations: {stations}") return url, _ = await self.station_parser.resolve_stream_url(self.config.stations[arg]) @@ -1060,16 +1011,16 @@ class RadioBot(slixmpp.ClientXMPP): await asyncio.sleep(0.1) self.jingle.set_audio_source(url, force_restart=True) - await self.reply(ctx, f"Playing: {arg}") + await self.reply(ctx, f"๐Ÿ“ป Playing: {arg}") except Exception as e: logger.error(f"Error playing station: {e}") - await self.reply(ctx, f"Failed: {e}") + await self.reply(ctx, f"โŒ Failed: {e}") else: - await self.reply(ctx, "Failed to connect to station") + await self.reply(ctx, "โŒ Failed to connect to station") async def cmd_stations(self, ctx: MessageContext, arg: str): stations = ', '.join(self.config.stations.keys()) or "None configured" - await self.reply(ctx, f"Stations: {stations}") + await self.reply(ctx, f"๐Ÿ“ป Stations: {stations}") async def cmd_list(self, ctx: MessageContext, arg: str): page_size = 15 @@ -1086,38 +1037,59 @@ class RadioBot(slixmpp.ClientXMPP): if tracks: text = "\n".join([f"{idx+1}. {track.display_name}" for idx, track in tracks]) - await self.reply(ctx, f"Playlist (Page {page}):\n{text}\n\nType '!list {page+1}' for more.") + await self.reply(ctx, f"๐Ÿ“‹ Playlist (Page {page}):\n{text}\n\nType '!list {page+1}' for more.") else: if page > 1: - await self.reply(ctx, f"No tracks on page {page}.") + await self.reply(ctx, f"๐Ÿ“‹ No tracks on page {page}.") else: - await self.reply(ctx, "Playlist is empty") + await self.reply(ctx, "๐Ÿ“‹ Playlist is empty") async def cmd_shuffle(self, ctx: MessageContext, arg: str): if self.playlist_manager.current_playlist: self.playlist_manager.current_playlist.shuffle() - await self.reply(ctx, "Playlist shuffled") + await self.reply(ctx, "๐Ÿ”€ Playlist shuffled") - async def cmd_search(self, ctx: MessageContext, arg: str): + async def _search_generic(self, ctx: MessageContext, query: str, source: str): if not self.ytdlp: - await self.reply(ctx, "yt-dlp is disabled") + await self.reply(ctx, "โŒ yt-dlp is disabled") return - if not arg: - await self.reply(ctx, "Usage: !search ") + if not query: + await self.reply(ctx, f"Usage: !{source} ") return - await self.reply(ctx, f"Searching: {arg}...") - res = await self.ytdlp.search(arg) + await self.reply(ctx, f"๐Ÿ” Searching {source}: {query}...") + res = await self.ytdlp.search(query, source=source) if res: self._search_results[ctx.sender_bare] = res - text = "\n".join([f"{i+1}. {r.title} [{r.duration_str}]" for i, r in enumerate(res[:5])]) - await self.reply(ctx, f"Results:\n{text}\n\nUse !dl # to download") + text = "\n".join([f"{i+1}. {r.title} [{r.duration_str}]" for i, r in enumerate(res[:10])]) + await self.reply(ctx, f"๐Ÿ” Results ({source}):\n{text}\n\nUse !dl # to download") else: - await self.reply(ctx, "No results found") + await self.reply(ctx, "โŒ No results found") + + async def cmd_search(self, ctx: MessageContext, arg: str): + await self._search_generic(ctx, arg, "youtube") + + async def cmd_sc(self, ctx: MessageContext, arg: str): + await self._search_generic(ctx, arg, "soundcloud") + + async def cmd_bc(self, ctx: MessageContext, arg: str): + await self._search_generic(ctx, arg, "bandcamp") + + async def cmd_library(self, ctx: MessageContext, arg: str): + if not arg: + await self.reply(ctx, "Usage: !lib ") + return + + results = self.playlist_manager.search_library(arg, limit=10) + if results: + text = "\n".join([f"{i+1}. {t.display_name}" for i, t in enumerate(results)]) + await self.reply(ctx, f"๐Ÿ“š Library Results:\n{text}\n\nUse !play to play.") + else: + await self.reply(ctx, "โŒ No tracks found in library") async def cmd_dl(self, ctx: MessageContext, arg: str): if not self.ytdlp: - await self.reply(ctx, "yt-dlp is disabled") + await self.reply(ctx, "โŒ yt-dlp is disabled") return if not arg: await self.reply(ctx, "Usage: !dl ") @@ -1135,23 +1107,23 @@ class RadioBot(slixmpp.ClientXMPP): results = self._search_results[ctx.sender_bare] if 0 <= idx < len(results): res = results[idx] - target_url = f"https://youtube.com/watch?v={res.id}" + target_url = f"https://youtube.com/watch?v={res.id}" if 'youtube' in res.url else res.url title = res.title except ValueError: pass if not target_url: - await self.reply(ctx, f"Searching: {arg}...") + await self.reply(ctx, f"๐Ÿ” Searching: {arg}...") res = await self.ytdlp.search(arg) if res: - target_url = f"https://youtube.com/watch?v={res[0].id}" + target_url = res[0].url title = res[0].title - await self.reply(ctx, f"Found: {title}") + await self.reply(ctx, f"๐ŸŽฏ Found: {title}") else: - await self.reply(ctx, "No results found") + await self.reply(ctx, "โŒ No results found") return - await self.reply(ctx, f"Downloading: {title}...") + await self.reply(ctx, f"โฌ‡๏ธ Downloading: {title}...") path = await self.ytdlp.download(target_url) if path: @@ -1159,38 +1131,38 @@ class RadioBot(slixmpp.ClientXMPP): if track: self.playlist_manager.library.append(track) self.playlist_manager.add_to_playlist(track) - await self.reply(ctx, f"Downloaded: {track.display_name}") + await self.reply(ctx, f"โœ… Downloaded: {track.display_name}") else: - await self.reply(ctx, "Downloaded file") + await self.reply(ctx, "โœ… Downloaded file") else: - await self.reply(ctx, "Download failed") + await self.reply(ctx, "โŒ Download failed") async def cmd_scan(self, ctx: MessageContext, arg: str): c = await self.playlist_manager.scan_library() self.playlist_manager.add_library_to_playlist() - await self.reply(ctx, f"Found {c} tracks") + await self.reply(ctx, f"โœ… Found {c} tracks") async def cmd_url(self, ctx: MessageContext, arg: str): if self.streamer: - await self.reply(ctx, f"Stream: {self.streamer.stream_url}") + await self.reply(ctx, f"๐Ÿ”— Stream: {self.streamer.stream_url}") else: - await self.reply(ctx, "HTTP streaming is disabled") + await self.reply(ctx, "โŒ HTTP streaming is disabled") async def cmd_callers(self, ctx: MessageContext, arg: str): if self.jingle: sessions = self.jingle.get_active_sessions() - await self.reply(ctx, f"Active callers: {len(sessions)}") + await self.reply(ctx, f"๐Ÿ“ž Active callers: {len(sessions)}") else: - await self.reply(ctx, "Jingle/calls are disabled") + await self.reply(ctx, "โŒ Jingle/calls are disabled") async def cmd_hangup(self, ctx: MessageContext, arg: str): if self.jingle: count = len(self.jingle.sessions) for s in list(self.jingle.sessions.values()): await self.jingle.stop_session(s.sid) - await self.reply(ctx, f"Ended {count} call(s)") + await self.reply(ctx, f"๐Ÿ”ด Ended {count} call(s)") else: - await self.reply(ctx, "Jingle/calls are disabled") + await self.reply(ctx, "โŒ Jingle/calls are disabled") async def cmd_memory(self, ctx: MessageContext, arg: str): mem_mb = self._get_memory_mb() @@ -1198,7 +1170,11 @@ class RadioBot(slixmpp.ClientXMPP): sessions = len(self.jingle.sessions) if self.jingle else 0 cache_size = len(self._search_results) - status = f"Memory Status:\nโ€ข Memory: {mem_mb:.1f} MB\nโ€ข Managed Tasks: {tasks}\nโ€ข Jingle Sessions: {sessions}\nโ€ข Search Cache: {cache_size}" + status = f"""๐Ÿ“Š **Memory Status:** +โ€ข Memory: {mem_mb:.1f} MB +โ€ข Managed Tasks: {tasks} +โ€ข Jingle Sessions: {sessions} +โ€ข Search Cache: {cache_size}""" if self.streamer: status += f"\nโ€ข HTTP Listeners: {self.streamer.listener_count}" @@ -1211,16 +1187,16 @@ class RadioBot(slixmpp.ClientXMPP): lines = [] if current: - lines.append(f"Now: {current.display_name}") + lines.append(f"โ–ถ๏ธ Now: {current.display_name}") if queue: - lines.append("\nUp Next:") + lines.append("\n๐Ÿ“‹ Up Next:") for i, track in enumerate(queue, 1): lines.append(f" {i}. {track.display_name}") else: - lines.append("\nQueue is empty") + lines.append("\n๐Ÿ“‹ Queue is empty") - lines.append(f"\n{self.playlist_manager.queue.size} in queue") + lines.append(f"\n๐Ÿ“Š {self.playlist_manager.queue.size} in queue") await self.reply(ctx, "\n".join(lines)) async def cmd_add_queue(self, ctx: MessageContext, arg: str): @@ -1232,11 +1208,11 @@ class RadioBot(slixmpp.ClientXMPP): if results: track = results[0] if self.playlist_manager.add_to_queue(track): - await self.reply(ctx, f"Added to queue: {track.display_name}") + await self.reply(ctx, f"โž• Added to queue: {track.display_name}") else: - await self.reply(ctx, "Queue is full") + await self.reply(ctx, "โŒ Queue is full") else: - await self.reply(ctx, "Not found in library. Try !dl first.") + await self.reply(ctx, "โŒ Not found in library. Try !dl first.") async def cmd_play_next(self, ctx: MessageContext, arg: str): if not arg: @@ -1247,15 +1223,15 @@ class RadioBot(slixmpp.ClientXMPP): if results: track = results[0] if self.playlist_manager.add_next_to_queue(track): - await self.reply(ctx, f"Playing next: {track.display_name}") + await self.reply(ctx, f"โญ๏ธ Playing next: {track.display_name}") else: - await self.reply(ctx, "Queue is full") + await self.reply(ctx, "โŒ Queue is full") else: - await self.reply(ctx, "Not found in library") + await self.reply(ctx, "โŒ Not found in library") async def cmd_clear_queue(self, ctx: MessageContext, arg: str): self.playlist_manager.clear_queue() - await self.reply(ctx, "Queue cleared") + await self.reply(ctx, "๐Ÿ—‘๏ธ Queue cleared") async def cmd_remove_queue(self, ctx: MessageContext, arg: str): if not arg: @@ -1266,29 +1242,27 @@ class RadioBot(slixmpp.ClientXMPP): idx = int(arg) - 1 track = self.playlist_manager.remove_from_queue(idx) if track: - await self.reply(ctx, f"Removed: {track.display_name}") + await self.reply(ctx, f"๐Ÿ—‘๏ธ Removed: {track.display_name}") else: - await self.reply(ctx, "Invalid index") + await self.reply(ctx, "โŒ Invalid index") except ValueError: - await self.reply(ctx, "Invalid number") + await self.reply(ctx, "โŒ Invalid number") async def cmd_shuffle_queue(self, ctx: MessageContext, arg: str): self.playlist_manager.shuffle_queue() - await self.reply(ctx, "Queue shuffled") + await self.reply(ctx, "๐Ÿ”€ Queue shuffled") async def cmd_history(self, ctx: MessageContext, arg: str): history = self.playlist_manager.queue.get_history(10) if history: - lines = ["Recently Played:"] + lines = ["๐Ÿ“œ Recently Played:"] for i, track in enumerate(reversed(history), 1): lines.append(f" {i}. {track.display_name}") await self.reply(ctx, "\n".join(lines)) else: - await self.reply(ctx, "No history yet") - + await self.reply(ctx, "๐Ÿ“œ No history yet") async def main(): - """Main execution function; handles CLI args and shutdown signals.""" import argparse p = argparse.ArgumentParser(description="XMPP Radio Bot") p.add_argument('-c', '--config', default='config.ini', help='Config file path') @@ -1307,7 +1281,6 @@ async def main(): except KeyboardInterrupt: logger.info("Shutting down...") finally: - # Graceful cleanup of background tasks if bot._gc_task: bot._gc_task.cancel() try: @@ -1341,8 +1314,7 @@ async def main(): gc.collect() gc.collect() - logger.info("Bot stopped cleanly") - + logger.info("๐Ÿ‘‹ Bot stopped cleanly") if __name__ == '__main__': asyncio.run(main()) \ No newline at end of file