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")