Compare commits
6 Commits
cbfd485c1f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| dcf10d85b3 | |||
| b8eb40bc64 | |||
| 70da956fd4 | |||
| 03e8589918 | |||
| 26c0b15513 | |||
| 04558151e8 |
@@ -138,6 +138,16 @@ class Assistant(commands.Cog):
|
|||||||
if not content and not image_urls:
|
if not content and not image_urls:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Vision is not supported by the configured text-only model. If the user
|
||||||
|
# sent image(s), tell them once and skip the images, continuing with any
|
||||||
|
# text so normal chat keeps working (avoids Ollama 400 "invalid image
|
||||||
|
# input" from sending images to a non-vision model).
|
||||||
|
if image_urls:
|
||||||
|
await message.reply("Images not supported at this time.")
|
||||||
|
image_urls = []
|
||||||
|
if not content:
|
||||||
|
return
|
||||||
|
|
||||||
if question_mode and not content.rstrip().endswith("?"):
|
if question_mode and not content.rstrip().endswith("?"):
|
||||||
if is_ping:
|
if is_ping:
|
||||||
await message.reply("Question mode is on - messages must end with **?**.")
|
await message.reply("Question mode is on - messages must end with **?**.")
|
||||||
|
|||||||
+162
-63
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
YTAudio — YouTube audio for Red-DiscordBot via yt-dlp -> mp3 -> Lavalink (local source).
|
YTAudio - YouTube audio for Red-DiscordBot via yt-dlp -> mp3 -> Lavalink (local source).
|
||||||
|
|
||||||
This cog OWNS the shared Red-Lavalink client connection:
|
This cog OWNS the shared Red-Lavalink client connection:
|
||||||
* On startup it calls ``lavalink.initialize()`` (once the bot is ready),
|
* On startup it calls ``lavalink.initialize()`` (once the bot is ready),
|
||||||
@@ -22,6 +22,7 @@ import functools
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import time as _time
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@@ -62,48 +63,70 @@ YTDLP_BASE_OPTS = {
|
|||||||
class QueuedTrack:
|
class QueuedTrack:
|
||||||
"""A track queued for a guild, resolved lazily at play time."""
|
"""A track queued for a guild, resolved lazily at play time."""
|
||||||
|
|
||||||
__slots__ = ("query", "requester", "title", "mp3_path")
|
__slots__ = ("query", "requester", "channel", "title", "mp3_path", "status_msg")
|
||||||
|
|
||||||
def __init__(self, query: str, requester: discord.abc.User):
|
def __init__(self, query: str, requester: discord.abc.User, channel):
|
||||||
self.query = query
|
self.query = query
|
||||||
self.requester = requester
|
self.requester = requester
|
||||||
|
self.channel = channel
|
||||||
self.title: Optional[str] = None
|
self.title: Optional[str] = None
|
||||||
self.mp3_path: Optional[str] = None
|
self.mp3_path: Optional[str] = None
|
||||||
|
self.status_msg = None
|
||||||
|
|
||||||
|
|
||||||
class GuildPlayer:
|
class GuildPlayer:
|
||||||
"""Per-guild queue + player-loop state. One per guild."""
|
"""Per-guild queue + player-loop state. One per guild.
|
||||||
|
|
||||||
|
The queue model is explicit and always inspectable:
|
||||||
|
* ``pending`` is the list of tracks waiting to play (index 0 is next).
|
||||||
|
* ``current`` is the track playing right now (or None).
|
||||||
|
Commands read these directly, so nowplaying/queue always reflect reality.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, cog: "YTAudio", guild_id: int):
|
def __init__(self, cog: "YTAudio", guild_id: int):
|
||||||
self.cog = cog
|
self.cog = cog
|
||||||
self.guild_id = guild_id
|
self.guild_id = guild_id
|
||||||
self.queue: "asyncio.Queue[Optional[QueuedTrack]]" = asyncio.Queue()
|
self.pending: List[QueuedTrack] = []
|
||||||
self.current: Optional[QueuedTrack] = None
|
self.current: Optional[QueuedTrack] = None
|
||||||
self.task: Optional[asyncio.Task] = None
|
self.task: Optional[asyncio.Task] = None
|
||||||
self.next_event = asyncio.Event()
|
self.next_event = asyncio.Event() # fired when the current track ends
|
||||||
|
self.wakeup = asyncio.Event() # fired when a track is added
|
||||||
self.volume = 100
|
self.volume = 100
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if self.task is None or self.task.done():
|
if self.task is None or self.task.done():
|
||||||
self.task = asyncio.create_task(self._player_loop())
|
self.task = asyncio.create_task(self._player_loop())
|
||||||
|
|
||||||
|
def enqueue(self, track: "QueuedTrack"):
|
||||||
|
self.pending.append(track)
|
||||||
|
self.wakeup.set()
|
||||||
|
|
||||||
|
def clear_pending(self):
|
||||||
|
cleared = self.pending
|
||||||
|
self.pending = []
|
||||||
|
for t in cleared:
|
||||||
|
self.cog._cleanup_file(t)
|
||||||
|
|
||||||
async def _player_loop(self):
|
async def _player_loop(self):
|
||||||
while True:
|
while True:
|
||||||
|
if not self.pending:
|
||||||
|
self.wakeup.clear()
|
||||||
|
if not self.pending:
|
||||||
|
try:
|
||||||
|
await self.wakeup.wait()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
continue
|
||||||
|
|
||||||
|
track = self.pending.pop(0)
|
||||||
|
self.current = track
|
||||||
self.next_event.clear()
|
self.next_event.clear()
|
||||||
try:
|
|
||||||
track = await self.queue.get()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
return
|
|
||||||
if track is None: # shutdown sentinel
|
|
||||||
return
|
|
||||||
|
|
||||||
self.current = track
|
|
||||||
try:
|
try:
|
||||||
await self.cog._play_track(self.guild_id, track, self.next_event)
|
await self.cog._play_track(self.guild_id, track, self.next_event)
|
||||||
# Wait until the track-end handler fires next_event
|
|
||||||
await self.next_event.wait()
|
await self.next_event.wait()
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self.cog._cleanup_file(track)
|
self.cog._cleanup_file(track)
|
||||||
|
self.current = None
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Error playing track in guild %s", self.guild_id)
|
log.exception("Error playing track in guild %s", self.guild_id)
|
||||||
@@ -137,7 +160,7 @@ class YTAudio(commands.Cog):
|
|||||||
async def _connect_lavalink(self):
|
async def _connect_lavalink(self):
|
||||||
await self.bot.wait_until_red_ready()
|
await self.bot.wait_until_red_ready()
|
||||||
# If a node is already connected (e.g. built-in Audio still loaded),
|
# If a node is already connected (e.g. built-in Audio still loaded),
|
||||||
# don't double-initialize — just share it.
|
# don't double-initialize, just share it.
|
||||||
if lavalink.get_all_nodes():
|
if lavalink.get_all_nodes():
|
||||||
self._ll_ready = True
|
self._ll_ready = True
|
||||||
log.info("YTAudio: a Lavalink node is already connected; sharing it.")
|
log.info("YTAudio: a Lavalink node is already connected; sharing it.")
|
||||||
@@ -195,10 +218,55 @@ class YTAudio(commands.Cog):
|
|||||||
gp.next_event.set()
|
gp.next_event.set()
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# yt-dlp resolution (blocking -> executor)
|
# yt-dlp resolution (blocking -> executor) with live progress
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def _blocking_extract(self, query: str) -> dict:
|
def _make_progress_hook(self, loop, track: QueuedTrack):
|
||||||
with yt_dlp.YoutubeDL(self._ytdlp_opts) as ydl:
|
# Runs inside the executor thread. Schedules throttled message edits
|
||||||
|
# back onto the event loop. Discord rate-limits edits, so we edit at
|
||||||
|
# most once per second and only when the whole-number percent changes.
|
||||||
|
state = {"last_edit": 0.0, "last_pct": -1}
|
||||||
|
|
||||||
|
def hook(d):
|
||||||
|
if track.status_msg is None:
|
||||||
|
return
|
||||||
|
status = d.get("status")
|
||||||
|
if status == "downloading":
|
||||||
|
total = d.get("total_bytes") or d.get("total_bytes_estimate")
|
||||||
|
downloaded = d.get("downloaded_bytes") or 0
|
||||||
|
if not total:
|
||||||
|
return
|
||||||
|
pct = int(downloaded * 100 / total)
|
||||||
|
now = _time.monotonic()
|
||||||
|
if pct == state["last_pct"] or (now - state["last_edit"]) < 1.0:
|
||||||
|
if pct < 100:
|
||||||
|
return
|
||||||
|
state["last_pct"] = pct
|
||||||
|
state["last_edit"] = now
|
||||||
|
content = "Downloading: {} ({}%)".format(track.query, pct)
|
||||||
|
loop.call_soon_threadsafe(
|
||||||
|
lambda c=content: asyncio.ensure_future(self._safe_edit(track, c))
|
||||||
|
)
|
||||||
|
elif status == "finished":
|
||||||
|
content = "Converting to mp3: {}".format(track.query)
|
||||||
|
loop.call_soon_threadsafe(
|
||||||
|
lambda c=content: asyncio.ensure_future(self._safe_edit(track, c))
|
||||||
|
)
|
||||||
|
|
||||||
|
return hook
|
||||||
|
|
||||||
|
async def _safe_edit(self, track: QueuedTrack, content: str):
|
||||||
|
if track.status_msg is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await track.status_msg.edit(content=content)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _blocking_extract(self, query: str, progress_hook=None) -> dict:
|
||||||
|
opts = dict(self._ytdlp_opts)
|
||||||
|
if progress_hook is not None:
|
||||||
|
opts["progress_hooks"] = [progress_hook]
|
||||||
|
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||||
info = ydl.extract_info(query, download=True)
|
info = ydl.extract_info(query, download=True)
|
||||||
if "entries" in info: # search/playlist -> first entry
|
if "entries" in info: # search/playlist -> first entry
|
||||||
info = info["entries"][0]
|
info = info["entries"][0]
|
||||||
@@ -206,8 +274,9 @@ class YTAudio(commands.Cog):
|
|||||||
|
|
||||||
async def _resolve_to_mp3(self, track: QueuedTrack):
|
async def _resolve_to_mp3(self, track: QueuedTrack):
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
hook = self._make_progress_hook(loop, track)
|
||||||
info = await loop.run_in_executor(
|
info = await loop.run_in_executor(
|
||||||
None, functools.partial(self._blocking_extract, track.query)
|
None, functools.partial(self._blocking_extract, track.query, hook)
|
||||||
)
|
)
|
||||||
track.title = info.get("title", track.query)
|
track.title = info.get("title", track.query)
|
||||||
vid = info.get("id")
|
vid = info.get("id")
|
||||||
@@ -226,6 +295,15 @@ class YTAudio(commands.Cog):
|
|||||||
# Playback
|
# Playback
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
async def _play_track(self, guild_id: int, track: QueuedTrack, done_event: asyncio.Event):
|
async def _play_track(self, guild_id: int, track: QueuedTrack, done_event: asyncio.Event):
|
||||||
|
status_msg = None
|
||||||
|
if track.channel is not None:
|
||||||
|
try:
|
||||||
|
status_msg = await track.channel.send(
|
||||||
|
"Downloading: {} (0%)".format(track.query)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
status_msg = None
|
||||||
|
track.status_msg = status_msg
|
||||||
await self._resolve_to_mp3(track)
|
await self._resolve_to_mp3(track)
|
||||||
player = lavalink.get_player(guild_id)
|
player = lavalink.get_player(guild_id)
|
||||||
gp = self.players.get(guild_id)
|
gp = self.players.get(guild_id)
|
||||||
@@ -234,11 +312,21 @@ class YTAudio(commands.Cog):
|
|||||||
loaded = await player.load_tracks(track.mp3_path)
|
loaded = await player.load_tracks(track.mp3_path)
|
||||||
if loaded.has_error or loaded.load_type != lavalink.enums.LoadType.TRACK_LOADED:
|
if loaded.has_error or loaded.load_type != lavalink.enums.LoadType.TRACK_LOADED:
|
||||||
log.error("YTAudio: failed to load mp3 into Lavalink: %s", track.mp3_path)
|
log.error("YTAudio: failed to load mp3 into Lavalink: %s", track.mp3_path)
|
||||||
|
if status_msg is not None:
|
||||||
|
try:
|
||||||
|
await status_msg.edit(content="Failed to load: {}".format(track.query))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
done_event.set()
|
done_event.set()
|
||||||
return
|
return
|
||||||
player.add(requester=track.requester, track=loaded.tracks[0])
|
player.add(requester=track.requester, track=loaded.tracks[0])
|
||||||
if not player.is_playing:
|
if not player.is_playing:
|
||||||
await player.play()
|
await player.play()
|
||||||
|
if status_msg is not None:
|
||||||
|
try:
|
||||||
|
await status_msg.edit(content="Now playing: {}".format(track.title or track.query))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _get_gp(self, guild_id: int) -> GuildPlayer:
|
def _get_gp(self, guild_id: int) -> GuildPlayer:
|
||||||
gp = self.players.get(guild_id)
|
gp = self.players.get(guild_id)
|
||||||
@@ -250,14 +338,22 @@ class YTAudio(commands.Cog):
|
|||||||
|
|
||||||
async def _ensure_connected(self, ctx: commands.Context) -> bool:
|
async def _ensure_connected(self, ctx: commands.Context) -> bool:
|
||||||
if not self._ll_ready and not lavalink.get_all_nodes():
|
if not self._ll_ready and not lavalink.get_all_nodes():
|
||||||
await ctx.send("❌ Lavalink is not connected yet. Try again in a moment.")
|
await ctx.send("Lavalink is not connected yet. Try again in a moment.")
|
||||||
return False
|
return False
|
||||||
if ctx.author.voice is None or ctx.author.voice.channel is None:
|
if ctx.author.voice is None or ctx.author.voice.channel is None:
|
||||||
await ctx.send("❌ You need to be in a voice channel.")
|
await ctx.send("You need to be in a voice channel.")
|
||||||
return False
|
return False
|
||||||
|
target = ctx.author.voice.channel
|
||||||
me_voice = ctx.guild.me.voice
|
me_voice = ctx.guild.me.voice
|
||||||
if me_voice is None or me_voice.channel != ctx.author.voice.channel:
|
if me_voice is None or me_voice.channel is None:
|
||||||
await lavalink.connect(ctx.author.voice.channel)
|
await lavalink.connect(target)
|
||||||
|
elif me_voice.channel.id != target.id:
|
||||||
|
# Already connected to a different channel: move instead of reconnecting.
|
||||||
|
try:
|
||||||
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
|
await player.move_to(target)
|
||||||
|
except Exception:
|
||||||
|
await lavalink.connect(target)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -271,23 +367,28 @@ class YTAudio(commands.Cog):
|
|||||||
return
|
return
|
||||||
gp = self._get_gp(ctx.guild.id)
|
gp = self._get_gp(ctx.guild.id)
|
||||||
gp.volume = await self.config.guild(ctx.guild).volume()
|
gp.volume = await self.config.guild(ctx.guild).volume()
|
||||||
track = QueuedTrack(query, ctx.author)
|
track = QueuedTrack(query, ctx.author, ctx.channel)
|
||||||
await gp.queue.put(track)
|
already_active = gp.current is not None or bool(gp.pending)
|
||||||
await ctx.send(f"➕ Queued: `{query}`")
|
gp.enqueue(track)
|
||||||
|
if already_active:
|
||||||
|
position = len(gp.pending)
|
||||||
|
await ctx.send("Added to queue (position {}): {}".format(position, query))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def skip(self, ctx: commands.Context):
|
async def skip(self, ctx: commands.Context):
|
||||||
"""Skip the current track."""
|
"""Skip the current track."""
|
||||||
|
gp = self.players.get(ctx.guild.id)
|
||||||
|
if gp is None or gp.current is None:
|
||||||
|
return await ctx.send("Nothing is playing.")
|
||||||
try:
|
try:
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
except Exception:
|
except Exception:
|
||||||
return await ctx.send("❌ Nothing is playing.")
|
return await ctx.send("Nothing is playing.")
|
||||||
|
# Stopping fires TRACK_END, which the event handler uses to advance
|
||||||
|
# the queue. Do not set next_event here or it would skip two tracks.
|
||||||
await player.stop()
|
await player.stop()
|
||||||
gp = self.players.get(ctx.guild.id)
|
await ctx.send("Skipped.")
|
||||||
if gp:
|
|
||||||
gp.next_event.set()
|
|
||||||
await ctx.send("⏭️ Skipped.")
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -295,19 +396,14 @@ class YTAudio(commands.Cog):
|
|||||||
"""Stop playback and clear the queue."""
|
"""Stop playback and clear the queue."""
|
||||||
gp = self.players.get(ctx.guild.id)
|
gp = self.players.get(ctx.guild.id)
|
||||||
if gp:
|
if gp:
|
||||||
while not gp.queue.empty():
|
gp.clear_pending()
|
||||||
try:
|
|
||||||
t = gp.queue.get_nowait()
|
|
||||||
self._cleanup_file(t)
|
|
||||||
except asyncio.QueueEmpty:
|
|
||||||
break
|
|
||||||
gp.next_event.set()
|
gp.next_event.set()
|
||||||
try:
|
try:
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
await player.stop()
|
await player.stop()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
await ctx.send("⏹️ Stopped and cleared the queue.")
|
await ctx.send("Stopped and cleared the queue.")
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -316,9 +412,9 @@ class YTAudio(commands.Cog):
|
|||||||
try:
|
try:
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
except Exception:
|
except Exception:
|
||||||
return await ctx.send("❌ Nothing is playing.")
|
return await ctx.send("Nothing is playing.")
|
||||||
await player.pause(True)
|
await player.pause(True)
|
||||||
await ctx.send("⏸️ Paused.")
|
await ctx.send("Paused.")
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -327,24 +423,31 @@ class YTAudio(commands.Cog):
|
|||||||
try:
|
try:
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
except Exception:
|
except Exception:
|
||||||
return await ctx.send("❌ Nothing is playing.")
|
return await ctx.send("Nothing is playing.")
|
||||||
await player.pause(False)
|
await player.pause(False)
|
||||||
await ctx.send("▶️ Resumed.")
|
await ctx.send("Resumed.")
|
||||||
|
|
||||||
@commands.command(name="queue")
|
@commands.command(name="queue", aliases=["q"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def queue_cmd(self, ctx: commands.Context):
|
async def queue_cmd(self, ctx: commands.Context):
|
||||||
"""Show the queue."""
|
"""Show the current track and the queue."""
|
||||||
gp = self.players.get(ctx.guild.id)
|
gp = self.players.get(ctx.guild.id)
|
||||||
if not gp or (gp.current is None and gp.queue.empty()):
|
if not gp or (gp.current is None and not gp.pending):
|
||||||
return await ctx.send("The queue is empty.")
|
return await ctx.send("The queue is empty.")
|
||||||
lines = []
|
lines = []
|
||||||
if gp.current:
|
if gp.current:
|
||||||
lines.append(f"**Now:** {gp.current.title or gp.current.query}")
|
lines.append("Now playing: {}".format(gp.current.title or gp.current.query))
|
||||||
pending: List[QueuedTrack] = list(gp.queue._queue) # snapshot
|
if gp.pending:
|
||||||
for i, t in enumerate(pending, 1):
|
lines.append("Up next:")
|
||||||
lines.append(f"{i}. {t.title or t.query}")
|
for i, t in enumerate(gp.pending, 1):
|
||||||
await ctx.send("\n".join(lines[:20]))
|
lines.append("{}. {}".format(i, t.title or t.query))
|
||||||
|
else:
|
||||||
|
lines.append("Queue is empty.")
|
||||||
|
# Keep within Discord message limits.
|
||||||
|
out = "\n".join(lines)
|
||||||
|
if len(out) > 1900:
|
||||||
|
out = out[:1900] + "\n..."
|
||||||
|
await ctx.send(out)
|
||||||
|
|
||||||
@commands.command(name="nowplaying", aliases=["np"])
|
@commands.command(name="nowplaying", aliases=["np"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -353,7 +456,7 @@ class YTAudio(commands.Cog):
|
|||||||
gp = self.players.get(ctx.guild.id)
|
gp = self.players.get(ctx.guild.id)
|
||||||
if not gp or gp.current is None:
|
if not gp or gp.current is None:
|
||||||
return await ctx.send("Nothing is playing.")
|
return await ctx.send("Nothing is playing.")
|
||||||
await ctx.send(f"🎵 {gp.current.title or gp.current.query}")
|
await ctx.send("Now playing: {}".format(gp.current.title or gp.current.query))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -369,26 +472,22 @@ class YTAudio(commands.Cog):
|
|||||||
await player.set_volume(level)
|
await player.set_volume(level)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
await ctx.send(f"🔊 Volume set to {level}.")
|
await ctx.send("Volume set to {}.".format(level))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command(name="summon", aliases=["ytjoin"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def join(self, ctx: commands.Context):
|
async def join(self, ctx: commands.Context):
|
||||||
"""Join your voice channel."""
|
"""Summon the bot to your voice channel."""
|
||||||
if await self._ensure_connected(ctx):
|
if await self._ensure_connected(ctx):
|
||||||
await ctx.send("✅ Joined.")
|
await ctx.send("Joined your voice channel.")
|
||||||
|
|
||||||
@commands.command(aliases=["disconnect", "dc"])
|
@commands.command(name="ytleave", aliases=["ytdc"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def leave(self, ctx: commands.Context):
|
async def leave(self, ctx: commands.Context):
|
||||||
"""Leave the voice channel and clear state."""
|
"""Leave the voice channel and clear state."""
|
||||||
gp = self.players.get(ctx.guild.id)
|
gp = self.players.get(ctx.guild.id)
|
||||||
if gp:
|
if gp:
|
||||||
while not gp.queue.empty():
|
gp.clear_pending()
|
||||||
try:
|
|
||||||
self._cleanup_file(gp.queue.get_nowait())
|
|
||||||
except asyncio.QueueEmpty:
|
|
||||||
break
|
|
||||||
gp.next_event.set()
|
gp.next_event.set()
|
||||||
try:
|
try:
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
@@ -396,7 +495,7 @@ class YTAudio(commands.Cog):
|
|||||||
await player.disconnect()
|
await player.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
await ctx.send("👋 Left the channel.")
|
await ctx.send("Left the channel.")
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Cleanup
|
# Cleanup
|
||||||
|
|||||||
Reference in New Issue
Block a user