feat: add multi-guild support and text channel mapping persistence via Docker volume

This commit is contained in:
mmacierzanka committed 2026-05-23 14:45:37 +02:00
1 parent 07e32c1a1d
commit c550215070
4 files changed
+250 -122

No files matched your search

+1
View File
@@ -3,3 +3,4 @@ __pycache__/
*.pyc *.pyc
downloads/ downloads/
.DS_Store .DS_Store
config/
+101 -58
View File
@@ -15,52 +15,80 @@ GUILD_ID = os.getenv('GUILD_ID')
class MusicBot(commands.Cog): class MusicBot(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.queue = [] self.queues = {} # guild_id -> list
self.current = None self.currents = {} # guild_id -> player
self.volume = 1.0 # Domyślna głośność ustawiona na 100% (1.0) self.volumes = {} # guild_id -> volume float
self.loop_current = False self.loop_currents = {} # guild_id -> bool
self.last_channel = None self.last_channels = {} # guild_id -> text channel
def load_saved_channel_id(self): def get_queue(self, guild_id: int):
if os.path.exists("config.json"): if guild_id not in self.queues:
self.queues[guild_id] = []
return self.queues[guild_id]
def get_current(self, guild_id: int):
return self.currents.get(guild_id)
def set_current(self, guild_id: int, player):
self.currents[guild_id] = player
def get_volume(self, guild_id: int):
return self.volumes.get(guild_id, 1.0)
def set_volume(self, guild_id: int, volume: float):
self.volumes[guild_id] = volume
def get_loop_current(self, guild_id: int):
return self.loop_currents.get(guild_id, False)
def set_loop_current(self, guild_id: int, loop: bool):
self.loop_currents[guild_id] = loop
def load_saved_channel_id(self, guild_id: int):
config_path = "config/config.json"
if os.path.exists(config_path):
try: try:
import json import json
with open("config.json", "r") as f: with open(config_path, "r") as f:
data = json.load(f) data = json.load(f)
return data.get("text_channel_id") val = data.get(str(guild_id))
return int(val) if val is not None else None
except Exception as e: except Exception as e:
print(f"Error loading config: {e}") print(f"Error loading config: {e}")
return None return None
def save_channel_id(self, channel_id): def save_channel_id(self, guild_id: int, channel_id: int):
config_path = "config/config.json"
try: try:
import json import json
os.makedirs(os.path.dirname(config_path), exist_ok=True)
data = {} data = {}
if os.path.exists("config.json"): if os.path.exists(config_path):
with open("config.json", "r") as f: with open(config_path, "r") as f:
data = json.load(f) data = json.load(f)
data["text_channel_id"] = channel_id data[str(guild_id)] = channel_id
with open("config.json", "w") as f: with open(config_path, "w") as f:
json.dump(data, f, indent=4) json.dump(data, f, indent=4)
except Exception as e: except Exception as e:
print(f"Error saving config: {e}") print(f"Error saving config: {e}")
def get_notification_channel(self): def get_notification_channel(self, guild_id: int):
if self.last_channel: if self.last_channels.get(guild_id):
return self.last_channel return self.last_channels[guild_id]
saved_id = self.load_saved_channel_id() saved_id = self.load_saved_channel_id(guild_id)
if saved_id: if saved_id:
channel = self.bot.get_channel(int(saved_id)) channel = self.bot.get_channel(int(saved_id))
if channel: if channel:
self.last_channel = channel self.last_channels[guild_id] = channel
return channel return channel
voice_client = self.bot.voice_clients[0] if self.bot.voice_clients else None # Fallback to the first accessible channel in the guild
if voice_client and voice_client.channel: guild = self.bot.get_guild(guild_id)
for tc in voice_client.channel.guild.text_channels: if guild:
if tc.permissions_for(voice_client.channel.guild.me).send_messages: for tc in guild.text_channels:
self.last_channel = tc if tc.permissions_for(guild.me).send_messages:
self.last_channels[guild_id] = tc
return tc return tc
return None return None
@@ -92,23 +120,24 @@ class MusicBot(commands.Cog):
return return
await interaction.response.defer() await interaction.response.defer()
self.last_channel = interaction.channel guild_id = interaction.guild.id
self.last_channels[guild_id] = interaction.channel
try: try:
player = await YTDLSource.from_url(query, loop=self.bot.loop, stream=True) player = await YTDLSource.from_url(query, loop=self.bot.loop, stream=True)
if interaction.guild.voice_client.is_playing(): if interaction.guild.voice_client.is_playing():
self.queue.append(player) self.get_queue(guild_id).append(player)
await interaction.followup.send( await interaction.followup.send(
f"Dodano do kolejki: {player.title}", f"Dodano do kolejki: {player.title}",
ephemeral=False ephemeral=False
) )
else: else:
self.current = player self.set_current(guild_id, player)
interaction.guild.voice_client.play( interaction.guild.voice_client.play(
player, player,
after=lambda e: asyncio.run_coroutine_threadsafe( after=lambda e: asyncio.run_coroutine_threadsafe(
self.play_next(), self.play_next(guild_id),
self.bot.loop self.bot.loop
) )
) )
@@ -122,45 +151,50 @@ class MusicBot(commands.Cog):
ephemeral=True ephemeral=True
) )
async def play_next(self): async def play_next(self, guild_id: int):
voice_client = self.bot.voice_clients[0] if self.bot.voice_clients else None voice_client = discord.utils.get(self.bot.voice_clients, guild__id=guild_id)
if not voice_client: if not voice_client:
self.current = None self.set_current(guild_id, None)
return return
if self.loop_current and self.current: loop_current = self.get_loop_current(guild_id)
current = self.get_current(guild_id)
if loop_current and current:
try: try:
url = self.current.data.get('webpage_url') or self.current.url url = current.data.get('webpage_url') or current.url
player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True) player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
self.current = player self.set_current(guild_id, player)
voice_client.play( voice_client.play(
self.current, player,
after=lambda e: asyncio.run_coroutine_threadsafe( after=lambda e: asyncio.run_coroutine_threadsafe(
self.play_next(), self.play_next(guild_id),
self.bot.loop self.bot.loop
) )
) )
await self.send_to_last_channel(f"Teraz odtwarzane (zapętlone): {self.current.title}") await self.send_to_last_channel(guild_id, f"Teraz odtwarzane (zapętlone): {player.title}")
return return
except Exception as e: except Exception as e:
print(f"Error repeating song: {e}") print(f"Error repeating song: {e}")
if len(self.queue) > 0: queue = self.get_queue(guild_id)
self.current = self.queue.pop(0) if len(queue) > 0:
next_player = queue.pop(0)
self.set_current(guild_id, next_player)
voice_client.play( voice_client.play(
self.current, next_player,
after=lambda e: asyncio.run_coroutine_threadsafe( after=lambda e: asyncio.run_coroutine_threadsafe(
self.play_next(), self.play_next(guild_id),
self.bot.loop self.bot.loop
) )
) )
await self.send_to_last_channel(f"Teraz odtwarzane: {self.current.title}") await self.send_to_last_channel(guild_id, f"Teraz odtwarzane: {next_player.title}")
else: else:
self.current = None self.set_current(guild_id, None)
await self.send_to_last_channel("Kolejka jest pusta.") await self.send_to_last_channel(guild_id, "Kolejka jest pusta.")
async def send_to_last_channel(self, text): async def send_to_last_channel(self, guild_id: int, text: str):
channel = self.get_notification_channel() channel = self.get_notification_channel(guild_id)
if channel: if channel:
try: try:
await channel.send(text) await channel.send(text)
@@ -170,14 +204,16 @@ class MusicBot(commands.Cog):
@app_commands.command(name="queue", description="Pokaż aktualną kolejkę") @app_commands.command(name="queue", description="Pokaż aktualną kolejkę")
async def show_queue(self, interaction: discord.Interaction): async def show_queue(self, interaction: discord.Interaction):
"""Pokaż aktualną kolejkę""" """Pokaż aktualną kolejkę"""
if not self.queue: guild_id = interaction.guild.id
queue = self.get_queue(guild_id)
if not queue:
await interaction.response.send_message( await interaction.response.send_message(
"Kolejka jest pusta.", "Kolejka jest pusta.",
ephemeral=True ephemeral=True
) )
return return
queue_list = "\n".join([f"{i + 1}. {item.title}" for i, item in enumerate(self.queue)]) queue_list = "\n".join([f"{i + 1}. {item.title}" for i, item in enumerate(queue)])
await interaction.response.send_message( await interaction.response.send_message(
f"Aktualna kolejka:\n{queue_list}", f"Aktualna kolejka:\n{queue_list}",
ephemeral=False ephemeral=False
@@ -231,10 +267,11 @@ class MusicBot(commands.Cog):
@app_commands.command(name="stop", description="Zatrzymaj odtwarzanie i wyczyść kolejkę") @app_commands.command(name="stop", description="Zatrzymaj odtwarzanie i wyczyść kolejkę")
async def stop(self, interaction: discord.Interaction): async def stop(self, interaction: discord.Interaction):
"""Zatrzymaj odtwarzanie i wyczyść kolejkę""" """Zatrzymaj odtwarzanie i wyczyść kolejkę"""
guild_id = interaction.guild.id
if interaction.guild.voice_client.is_playing(): if interaction.guild.voice_client.is_playing():
interaction.guild.voice_client.stop() interaction.guild.voice_client.stop()
self.queue.clear() self.get_queue(guild_id).clear()
self.current = None self.set_current(guild_id, None)
await interaction.response.send_message( await interaction.response.send_message(
"Zatrzymano odtwarzanie i wyczyszczono kolejkę.", "Zatrzymano odtwarzanie i wyczyszczono kolejkę.",
ephemeral=False ephemeral=False
@@ -244,6 +281,7 @@ class MusicBot(commands.Cog):
@app_commands.describe(volume="Poziom głośności (1-100)") @app_commands.describe(volume="Poziom głośności (1-100)")
async def volume(self, interaction: discord.Interaction, volume: int): async def volume(self, interaction: discord.Interaction, volume: int):
"""Zmień głośność odtwarzacza""" """Zmień głośność odtwarzacza"""
guild_id = interaction.guild.id
if interaction.guild.voice_client is None: if interaction.guild.voice_client is None:
return await interaction.response.send_message( return await interaction.response.send_message(
"Nie połączono z kanałem głosowym.", "Nie połączono z kanałem głosowym.",
@@ -251,8 +289,8 @@ class MusicBot(commands.Cog):
) )
if 0 < volume <= 100: if 0 < volume <= 100:
self.volume = volume / 100 self.set_volume(guild_id, volume / 100)
interaction.guild.voice_client.source.volume = self.volume interaction.guild.voice_client.source.volume = self.get_volume(guild_id)
await interaction.response.send_message( await interaction.response.send_message(
f"Zmieniono głośność na {volume}%", f"Zmieniono głośność na {volume}%",
ephemeral=False ephemeral=False
@@ -268,9 +306,11 @@ class MusicBot(commands.Cog):
@app_commands.command(name="nowplaying", description="Pokaż aktualnie odtwarzany utwór") @app_commands.command(name="nowplaying", description="Pokaż aktualnie odtwarzany utwór")
async def now_playing(self, interaction: discord.Interaction): async def now_playing(self, interaction: discord.Interaction):
"""Pokaż aktualnie odtwarzany utwór""" """Pokaż aktualnie odtwarzany utwór"""
if self.current: guild_id = interaction.guild.id
current = self.get_current(guild_id)
if current:
await interaction.response.send_message( await interaction.response.send_message(
f"Teraz odtwarzane: {self.current.title}", f"Teraz odtwarzane: {current.title}",
ephemeral=False ephemeral=False
) )
else: else:
@@ -282,10 +322,11 @@ class MusicBot(commands.Cog):
@app_commands.command(name="leave", description="Odłącz bota od kanału głosowego") @app_commands.command(name="leave", description="Odłącz bota od kanału głosowego")
async def leave(self, interaction: discord.Interaction): async def leave(self, interaction: discord.Interaction):
"""Odłącz bota od kanału głosowego""" """Odłącz bota od kanału głosowego"""
guild_id = interaction.guild.id
if interaction.guild.voice_client is not None: if interaction.guild.voice_client is not None:
await interaction.guild.voice_client.disconnect() await interaction.guild.voice_client.disconnect()
self.queue.clear() self.get_queue(guild_id).clear()
self.current = None self.set_current(guild_id, None)
await interaction.response.send_message( await interaction.response.send_message(
"Rozłączono z kanału głosowego.", "Rozłączono z kanału głosowego.",
ephemeral=False ephemeral=False
@@ -299,8 +340,10 @@ class MusicBot(commands.Cog):
@app_commands.command(name="loop", description="Włącz/wyłącz zapętlanie aktualnego utworu") @app_commands.command(name="loop", description="Włącz/wyłącz zapętlanie aktualnego utworu")
async def loop_command(self, interaction: discord.Interaction): async def loop_command(self, interaction: discord.Interaction):
"""Włącz/wyłącz zapętlanie aktualnego utworu""" """Włącz/wyłącz zapętlanie aktualnego utworu"""
self.loop_current = not self.loop_current guild_id = interaction.guild.id
state = "włączone" if self.loop_current else "wyłączone" loop_current = not self.get_loop_current(guild_id)
self.set_loop_current(guild_id, loop_current)
state = "włączone" if loop_current else "wyłączone"
await interaction.response.send_message( await interaction.response.send_message(
f"Zapętlanie aktualnego utworu zostało {state}.", f"Zapętlanie aktualnego utworu zostało {state}.",
ephemeral=False ephemeral=False
+2
View File
@@ -8,4 +8,6 @@ services:
- "80:8000" - "80:8000"
environment: environment:
- DISCORD_TOKEN=${DISCORD_TOKEN} - DISCORD_TOKEN=${DISCORD_TOKEN}
volumes:
- ./config:/app/config
restart: unless-stopped restart: unless-stopped
+146 -64
View File
@@ -3,6 +3,7 @@ from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Optional from typing import List, Optional
import asyncio import asyncio
import discord
# Import bot instance and YTDL class # Import bot instance and YTDL class
from bot import bot from bot import bot
@@ -33,6 +34,15 @@ class ConnectRequest(BaseModel):
class TextChannelRequest(BaseModel): class TextChannelRequest(BaseModel):
channel_id: str channel_id: str
# Helper to determine active guild
def get_active_guild():
voice_client = bot.voice_clients[0] if bot.voice_clients else None
if voice_client:
return voice_client.guild
elif bot.guilds:
return bot.guilds[0]
return None
# FastAPI Endpoints # FastAPI Endpoints
@app.get("/api/status") @app.get("/api/status")
async def get_status(): async def get_status():
@@ -40,7 +50,25 @@ async def get_status():
if not music_cog: if not music_cog:
return {"error": "MusicBot cog not loaded"} return {"error": "MusicBot cog not loaded"}
voice_client = bot.voice_clients[0] if bot.voice_clients else None guild = get_active_guild()
if not guild:
return {
"playing": False,
"paused": False,
"loop_single": False,
"volume": 100,
"current": None,
"queue": [],
"voice_channels": [],
"active_voice_channel": None,
"text_channels": [],
"active_text_channel_id": None
}
guild_id = guild.id
# Active voice channel
voice_client = discord.utils.get(bot.voice_clients, guild__id=guild_id)
active_vc = None active_vc = None
if voice_client and voice_client.channel: if voice_client and voice_client.channel:
active_vc = { active_vc = {
@@ -50,53 +78,54 @@ async def get_status():
"guild_id": str(voice_client.channel.guild.id) "guild_id": str(voice_client.channel.guild.id)
} }
# Gather voice channels available to the bot across all guilds # Gather voice channels available to the bot on this active guild
channels = [] channels = []
for guild in bot.guilds: for channel in guild.voice_channels:
for channel in guild.voice_channels: channels.append({
channels.append({ "guild_name": guild.name,
"guild_id": str(guild.id),
"channel_name": channel.name,
"channel_id": str(channel.id)
})
# Gather text channels available to the bot on this active guild where it can write
text_channels = []
for channel in guild.text_channels:
if channel.permissions_for(guild.me).send_messages:
text_channels.append({
"guild_name": guild.name, "guild_name": guild.name,
"guild_id": str(guild.id), "guild_id": str(guild.id),
"channel_name": channel.name, "channel_name": channel.name,
"channel_id": str(channel.id) "channel_id": str(channel.id)
}) })
# Gather text channels available to the bot across all guilds where it has send permissions
text_channels = []
for guild in bot.guilds:
for channel in guild.text_channels:
if channel.permissions_for(guild.me).send_messages:
text_channels.append({
"guild_name": guild.name,
"guild_id": str(guild.id),
"channel_name": channel.name,
"channel_id": str(channel.id)
})
active_tc_id = None active_tc_id = None
active_tc = music_cog.get_notification_channel() active_tc = music_cog.get_notification_channel(guild_id)
if active_tc: if active_tc:
active_tc_id = str(active_tc.id) active_tc_id = str(active_tc.id)
playing = voice_client.is_playing() if voice_client else False playing = voice_client.is_playing() if voice_client else False
paused = voice_client.is_paused() if voice_client else False paused = voice_client.is_paused() if voice_client else False
current_track = music_cog.get_current(guild_id)
queue = music_cog.get_queue(guild_id)
return { return {
"playing": playing, "playing": playing,
"paused": paused, "paused": paused,
"loop_single": music_cog.loop_current, "loop_single": music_cog.get_loop_current(guild_id),
"volume": int(music_cog.volume * 100), "volume": int(music_cog.get_volume(guild_id) * 100),
"current": { "current": {
"title": music_cog.current.title, "title": current_track.title,
"url": music_cog.current.data.get("webpage_url") or music_cog.current.url, "url": current_track.data.get("webpage_url") or current_track.url,
"id": music_cog.current.id "id": current_track.id
} if music_cog.current else None, } if current_track else None,
"queue": [ "queue": [
{ {
"title": item.title, "title": item.title,
"url": item.data.get("webpage_url") or item.url, "url": item.data.get("webpage_url") or item.url,
"id": item.id "id": item.id
} for item in music_cog.queue } for item in queue
], ],
"voice_channels": channels, "voice_channels": channels,
"active_voice_channel": active_vc, "active_voice_channel": active_vc,
@@ -110,54 +139,71 @@ async def api_play(req: PlayRequest):
if not music_cog: if not music_cog:
raise HTTPException(status_code=500, detail="MusicBot cog not loaded") raise HTTPException(status_code=500, detail="MusicBot cog not loaded")
voice_client = bot.voice_clients[0] if bot.voice_clients else None guild = get_active_guild()
if not guild:
raise HTTPException(status_code=400, detail="Bot has no active server context")
guild_id = guild.id
voice_client = discord.utils.get(bot.voice_clients, guild__id=guild_id)
if not voice_client: if not voice_client:
raise HTTPException(status_code=400, detail="Bot is not connected to a voice channel") raise HTTPException(status_code=400, detail="Bot is not connected to a voice channel on this server")
try: try:
player = await YTDLSource.from_url(req.query, loop=bot.loop, stream=True) player = await YTDLSource.from_url(req.query, loop=bot.loop, stream=True)
if voice_client.is_playing() or voice_client.is_paused(): if voice_client.is_playing() or voice_client.is_paused():
music_cog.queue.append(player) music_cog.get_queue(guild_id).append(player)
await music_cog.send_to_last_channel(f"Dodano do kolejki (Web): {player.title}") await music_cog.send_to_last_channel(guild_id, f"Dodano do kolejki (Web): {player.title}")
return {"status": "queued", "title": player.title} return {"status": "queued", "title": player.title}
else: else:
music_cog.current = player music_cog.set_current(guild_id, player)
voice_client.play( voice_client.play(
player, player,
after=lambda e: asyncio.run_coroutine_threadsafe( after=lambda e: asyncio.run_coroutine_threadsafe(
music_cog.play_next(), music_cog.play_next(guild_id),
bot.loop bot.loop
) )
) )
await music_cog.send_to_last_channel(f"Teraz odtwarzane (Web): {player.title}") await music_cog.send_to_last_channel(guild_id, f"Teraz odtwarzane (Web): {player.title}")
return {"status": "playing", "title": player.title} return {"status": "playing", "title": player.title}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/pause") @app.post("/api/pause")
async def api_pause(): async def api_pause():
voice_client = bot.voice_clients[0] if bot.voice_clients else None guild = get_active_guild()
if not guild:
raise HTTPException(status_code=400, detail="Bot has no active server context")
voice_client = discord.utils.get(bot.voice_clients, guild__id=guild.id)
if voice_client and voice_client.is_playing(): if voice_client and voice_client.is_playing():
voice_client.pause() voice_client.pause()
return {"status": "paused"} return {"status": "paused"}
raise HTTPException(status_code=400, detail="Bot is not playing") raise HTTPException(status_code=400, detail="Bot is not playing on this server")
@app.post("/api/resume") @app.post("/api/resume")
async def api_resume(): async def api_resume():
voice_client = bot.voice_clients[0] if bot.voice_clients else None guild = get_active_guild()
if not guild:
raise HTTPException(status_code=400, detail="Bot has no active server context")
voice_client = discord.utils.get(bot.voice_clients, guild__id=guild.id)
if voice_client and voice_client.is_paused(): if voice_client and voice_client.is_paused():
voice_client.resume() voice_client.resume()
return {"status": "resumed"} return {"status": "resumed"}
raise HTTPException(status_code=400, detail="Bot is not paused") raise HTTPException(status_code=400, detail="Bot is not paused on this server")
@app.post("/api/skip") @app.post("/api/skip")
async def api_skip(): async def api_skip():
voice_client = bot.voice_clients[0] if bot.voice_clients else None guild = get_active_guild()
if not guild:
raise HTTPException(status_code=400, detail="Bot has no active server context")
voice_client = discord.utils.get(bot.voice_clients, guild__id=guild.id)
if voice_client and (voice_client.is_playing() or voice_client.is_paused()): if voice_client and (voice_client.is_playing() or voice_client.is_paused()):
voice_client.stop() voice_client.stop()
return {"status": "skipped"} return {"status": "skipped"}
raise HTTPException(status_code=400, detail="Bot is not playing anything") raise HTTPException(status_code=400, detail="Bot is not playing anything on this server")
@app.post("/api/stop") @app.post("/api/stop")
async def api_stop(): async def api_stop():
@@ -165,12 +211,17 @@ async def api_stop():
if not music_cog: if not music_cog:
raise HTTPException(status_code=500, detail="MusicBot cog not loaded") raise HTTPException(status_code=500, detail="MusicBot cog not loaded")
voice_client = bot.voice_clients[0] if bot.voice_clients else None guild = get_active_guild()
if not guild:
return {"status": "stopped"}
guild_id = guild.id
voice_client = discord.utils.get(bot.voice_clients, guild__id=guild_id)
if voice_client and (voice_client.is_playing() or voice_client.is_paused()): if voice_client and (voice_client.is_playing() or voice_client.is_paused()):
voice_client.stop() voice_client.stop()
music_cog.queue.clear() music_cog.get_queue(guild_id).clear()
music_cog.current = None music_cog.set_current(guild_id, None)
return {"status": "stopped"} return {"status": "stopped"}
@app.post("/api/volume") @app.post("/api/volume")
@@ -179,11 +230,16 @@ async def api_volume(req: VolumeRequest):
if not music_cog: if not music_cog:
raise HTTPException(status_code=500, detail="MusicBot cog not loaded") raise HTTPException(status_code=500, detail="MusicBot cog not loaded")
guild = get_active_guild()
if not guild:
raise HTTPException(status_code=400, detail="Bot has no active server context")
guild_id = guild.id
if 0 <= req.volume <= 100: if 0 <= req.volume <= 100:
music_cog.volume = req.volume / 100 music_cog.set_volume(guild_id, req.volume / 100)
voice_client = bot.voice_clients[0] if bot.voice_clients else None voice_client = discord.utils.get(bot.voice_clients, guild__id=guild_id)
if voice_client and voice_client.source: if voice_client and voice_client.source:
voice_client.source.volume = music_cog.volume voice_client.source.volume = music_cog.get_volume(guild_id)
return {"status": "success", "volume": req.volume} return {"status": "success", "volume": req.volume}
raise HTTPException(status_code=400, detail="Volume must be between 0 and 100") raise HTTPException(status_code=400, detail="Volume must be between 0 and 100")
@@ -193,7 +249,11 @@ async def api_loop(req: LoopRequest):
if not music_cog: if not music_cog:
raise HTTPException(status_code=500, detail="MusicBot cog not loaded") raise HTTPException(status_code=500, detail="MusicBot cog not loaded")
music_cog.loop_current = req.loop guild = get_active_guild()
if not guild:
raise HTTPException(status_code=400, detail="Bot has no active server context")
music_cog.set_loop_current(guild.id, req.loop)
return {"status": "success", "loop": req.loop} return {"status": "success", "loop": req.loop}
@app.post("/api/remove") @app.post("/api/remove")
@@ -202,10 +262,17 @@ async def api_remove(req: RemoveRequest):
if not music_cog: if not music_cog:
raise HTTPException(status_code=500, detail="MusicBot cog not loaded") raise HTTPException(status_code=500, detail="MusicBot cog not loaded")
initial_len = len(music_cog.queue) guild = get_active_guild()
music_cog.queue = [item for item in music_cog.queue if item.id != req.id] if not guild:
raise HTTPException(status_code=400, detail="Bot has no active server context")
if len(music_cog.queue) < initial_len: guild_id = guild.id
queue = music_cog.get_queue(guild_id)
initial_len = len(queue)
new_queue = [item for item in queue if item.id != req.id]
music_cog.queues[guild_id] = new_queue
if len(new_queue) < initial_len:
return {"status": "removed"} return {"status": "removed"}
raise HTTPException(status_code=404, detail="Track not found in queue") raise HTTPException(status_code=404, detail="Track not found in queue")
@@ -215,7 +282,13 @@ async def api_reorder(req: ReorderRequest):
if not music_cog: if not music_cog:
raise HTTPException(status_code=500, detail="MusicBot cog not loaded") raise HTTPException(status_code=500, detail="MusicBot cog not loaded")
queue_map = {item.id: item for item in music_cog.queue} guild = get_active_guild()
if not guild:
raise HTTPException(status_code=400, detail="Bot has no active server context")
guild_id = guild.id
queue = music_cog.get_queue(guild_id)
queue_map = {item.id: item for item in queue}
new_queue = [] new_queue = []
for track_id in req.queue_ids: for track_id in req.queue_ids:
@@ -223,11 +296,11 @@ async def api_reorder(req: ReorderRequest):
new_queue.append(queue_map[track_id]) new_queue.append(queue_map[track_id])
# Retain any items not present in the reordered IDs list # Retain any items not present in the reordered IDs list
for item in music_cog.queue: for item in queue:
if item.id not in req.queue_ids: if item.id not in req.queue_ids:
new_queue.append(item) new_queue.append(item)
music_cog.queue = new_queue music_cog.queues[guild_id] = new_queue
return {"status": "success"} return {"status": "success"}
@app.post("/api/connect") @app.post("/api/connect")
@@ -241,35 +314,43 @@ async def api_connect(req: ConnectRequest):
if not channel: if not channel:
raise HTTPException(status_code=404, detail="Voice channel not found") raise HTTPException(status_code=404, detail="Voice channel not found")
if bot.voice_clients: guild_id = channel.guild.id
for vc in bot.voice_clients:
# Only disconnect voice clients from the SAME guild!
for vc in bot.voice_clients:
if vc.guild.id == guild_id:
await vc.disconnect() await vc.disconnect()
await channel.connect() await channel.connect()
music_cog = bot.get_cog("MusicBot") music_cog = bot.get_cog("MusicBot")
if music_cog and not music_cog.last_channel: if music_cog and not music_cog.get_notification_channel(guild_id):
# Save first accessible text channel to send updates # Save first accessible text channel to send updates
for tc in channel.guild.text_channels: for tc in channel.guild.text_channels:
if tc.permissions_for(channel.guild.me).send_messages: if tc.permissions_for(channel.guild.me).send_messages:
music_cog.last_channel = tc music_cog.last_channels[guild_id] = tc
break break
return {"status": "connected", "channel": channel.name} return {"status": "connected", "channel": channel.name}
@app.post("/api/disconnect") @app.post("/api/disconnect")
async def api_disconnect(): async def api_disconnect():
if bot.voice_clients: guild = get_active_guild()
for vc in bot.voice_clients: if not guild:
await vc.disconnect() raise HTTPException(status_code=400, detail="Bot has no active server context")
guild_id = guild.id
voice_client = discord.utils.get(bot.voice_clients, guild__id=guild_id)
if voice_client:
await voice_client.disconnect()
music_cog = bot.get_cog("MusicBot") music_cog = bot.get_cog("MusicBot")
if music_cog: if music_cog:
music_cog.queue.clear() music_cog.get_queue(guild_id).clear()
music_cog.current = None music_cog.set_current(guild_id, None)
return {"status": "disconnected"} return {"status": "disconnected"}
raise HTTPException(status_code=400, detail="Bot not connected to voice") raise HTTPException(status_code=400, detail="Bot not connected to voice on this server")
@app.post("/api/settings/text_channel") @app.post("/api/settings/text_channel")
async def api_set_text_channel(req: TextChannelRequest): async def api_set_text_channel(req: TextChannelRequest):
@@ -281,10 +362,11 @@ async def api_set_text_channel(req: TextChannelRequest):
if not channel: if not channel:
raise HTTPException(status_code=404, detail="Text channel not found") raise HTTPException(status_code=404, detail="Text channel not found")
music_cog.last_channel = channel guild_id = channel.guild.id
music_cog.save_channel_id(int(req.channel_id)) music_cog.last_channels[guild_id] = channel
music_cog.save_channel_id(guild_id, int(req.channel_id))
# Send test message to confirm in Polish with new bot name # Send test message to confirm
try: try:
await channel.send(f"🔔 Ten kanał został ustawiony jako główny kanał powiadomień dla ŻyrarBOT z panelu webowego.") await channel.send(f"🔔 Ten kanał został ustawiony jako główny kanał powiadomień dla ŻyrarBOT z panelu webowego.")
except Exception as e: except Exception as e: