diff --git a/bot.py b/bot.py
index eb8b862..bfec921 100644
--- a/bot.py
+++ b/bot.py
@@ -19,6 +19,7 @@ class MusicBot(commands.Cog):
self.currents = {} # guild_id -> player
self.volumes = {} # guild_id -> volume float
self.loop_currents = {} # guild_id -> bool
+ self.loop_playlists = {} # guild_id -> bool
self.last_channels = {} # guild_id -> text channel
self.history = {} # guild_id -> list of dicts
self.on_state_change_callback = None
@@ -94,6 +95,12 @@ class MusicBot(commands.Cog):
def set_loop_current(self, guild_id: int, loop: bool):
self.loop_currents[guild_id] = loop
+ def get_loop_playlist(self, guild_id: int):
+ return self.loop_playlists.get(guild_id, False)
+
+ def set_loop_playlist(self, guild_id: int, loop: bool):
+ self.loop_playlists[guild_id] = loop
+
async def trigger_state_change(self):
if self.on_state_change_callback:
try:
@@ -307,6 +314,17 @@ class MusicBot(commands.Cog):
return
except Exception as e:
print(f"Error repeating song: {e}")
+ elif self.get_loop_playlist(guild_id) and current:
+ try:
+ if current.data.get('is_local'):
+ new_player = await YTDLSource.from_file(current.data['filepath'], loop=self.bot.loop, title=current.title)
+ else:
+ url = current.data.get('webpage_url') or current.url
+ new_player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
+ self.get_queue(guild_id).append(new_player)
+ await self.trigger_state_change()
+ except Exception as e:
+ print(f"Error re-adding track to playlist loop: {e}")
queue = self.get_queue(guild_id)
if len(queue) > 0:
@@ -573,12 +591,28 @@ class MusicBot(commands.Cog):
guild_id = interaction.guild.id
loop_current = not self.get_loop_current(guild_id)
self.set_loop_current(guild_id, loop_current)
+ if loop_current:
+ self.set_loop_playlist(guild_id, False)
state = "włączone" if loop_current else "wyłączone"
await interaction.response.send_message(
f"Zapętlanie aktualnego utworu zostało {state}.",
ephemeral=False
)
+ @app_commands.command(name="loopplaylist", description="Włącz/wyłącz zapętlanie całej kolejki (tryb playlisty)")
+ async def loopplaylist_command(self, interaction: discord.Interaction):
+ """Włącz/wyłącz zapętlanie całej kolejki"""
+ guild_id = interaction.guild.id
+ loop_pl = not self.get_loop_playlist(guild_id)
+ self.set_loop_playlist(guild_id, loop_pl)
+ if loop_pl:
+ self.set_loop_current(guild_id, False)
+ state = "włączone (utwory po zakończeniu wracają na koniec kolejki)" if loop_pl else "wyłączone"
+ await interaction.response.send_message(
+ f"🔁 Zapętlanie całej kolejki: **{state}**.",
+ ephemeral=False
+ )
+
class MyBot(commands.Bot):
def __init__(self):
diff --git a/static/app.js b/static/app.js
index 2c510fa..f8f81e8 100644
--- a/static/app.js
+++ b/static/app.js
@@ -28,6 +28,7 @@ const playPauseIcon = document.getElementById('play-pause-icon');
const btnSkip = document.getElementById('btn-skip');
const btnStop = document.getElementById('btn-stop');
const btnLoop = document.getElementById('btn-loop');
+const btnLoopPlaylist = document.getElementById('btn-loop-playlist');
const vinylRecord = document.getElementById('vinyl-record');
const musicWave = document.getElementById('music-wave');
const volumeSlider = document.getElementById('volume-slider');
@@ -138,6 +139,9 @@ function bindEvents() {
btnSkip.addEventListener('click', skipTrack);
btnStop.addEventListener('click', stopPlayback);
btnLoop.addEventListener('click', toggleLoop);
+ if (btnLoopPlaylist) {
+ btnLoopPlaylist.addEventListener('click', toggleLoopPlaylist);
+ }
// Volume slider
volumeSlider.addEventListener('input', (e) => {
@@ -385,9 +389,25 @@ function updateUI(status) {
if (status.loop_single) {
loopBadge.style.display = 'flex';
btnLoop.classList.add('active');
+ btnLoop.style.background = 'var(--primary)';
+ btnLoop.style.color = 'var(--bg-card)';
} else {
loopBadge.style.display = 'none';
btnLoop.classList.remove('active');
+ btnLoop.style.background = '';
+ btnLoop.style.color = '';
+ }
+
+ if (btnLoopPlaylist) {
+ if (status.loop_playlist) {
+ btnLoopPlaylist.classList.add('active');
+ btnLoopPlaylist.style.background = 'var(--primary)';
+ btnLoopPlaylist.style.color = 'var(--bg-card)';
+ } else {
+ btnLoopPlaylist.classList.remove('active');
+ btnLoopPlaylist.style.background = '';
+ btnLoopPlaylist.style.color = '';
+ }
}
// 6. Queue render (if not currently dragging)
@@ -707,6 +727,18 @@ async function toggleLoop() {
}
}
+// Toggle Playlist Loop State
+async function toggleLoopPlaylist() {
+ try {
+ const res = await fetch('/api/loop_playlist', { method: 'POST' });
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.detail || 'Błąd zapętlania');
+ showToast(data.loop_playlist ? 'Włączono zapętlanie całej kolejki (playlisty)!' : 'Wyłączono zapętlanie kolejki.', 'info');
+ } catch (err) {
+ showToast('Nie udało się zmienić trybu zapętlania', 'error');
+ }
+}
+
// Set Volume
async function setVolume(vol) {
try {
diff --git a/static/index.html b/static/index.html
index 5d19117..b276b99 100644
--- a/static/index.html
+++ b/static/index.html
@@ -118,6 +118,9 @@
+
diff --git a/web.py b/web.py
index 7ceef09..f95fd9c 100644
--- a/web.py
+++ b/web.py
@@ -141,6 +141,7 @@ async def get_status():
"playing": playing,
"paused": paused,
"loop_single": music_cog.get_loop_current(guild_id),
+ "loop_playlist": music_cog.get_loop_playlist(guild_id),
"volume": int(music_cog.get_volume(guild_id) * 100),
"current": {
"title": current_track.title,
@@ -356,9 +357,29 @@ async def api_loop(req: LoopRequest):
raise HTTPException(status_code=400, detail="Bot has no active server context")
music_cog.set_loop_current(guild.id, req.loop)
+ if req.loop:
+ music_cog.set_loop_playlist(guild.id, False)
await broadcast_status()
return {"status": "success", "loop": req.loop}
+@app.post("/api/loop_playlist")
+async def api_loop_playlist():
+ music_cog = bot.get_cog("MusicBot")
+ if not music_cog:
+ 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")
+
+ current_state = music_cog.get_loop_playlist(guild.id)
+ new_state = not current_state
+ music_cog.set_loop_playlist(guild.id, new_state)
+ if new_state:
+ music_cog.set_loop_current(guild.id, False)
+ await broadcast_status()
+ return {"status": "success", "loop_playlist": new_state}
+
@app.post("/api/remove")
async def api_remove(req: RemoveRequest):
music_cog = bot.get_cog("MusicBot")