diff --git a/bot.py b/bot.py index 6404760..867529c 100644 --- a/bot.py +++ b/bot.py @@ -25,6 +25,7 @@ class MusicBot(commands.Cog): self.history = {} # guild_id -> list of dicts self.queue_indices = {} # guild_id -> int self.jump_flag = {} # guild_id -> bool + self.default_plays = self.get_default_plays() self.on_state_change_callback = None def load_history(self, guild_id: int): @@ -126,15 +127,21 @@ class MusicBot(commands.Cog): async def get_fresh_player(self, track): if track.data.get('is_local'): - return await YTDLSource.from_file(track.data['filepath'], loop=self.bot.loop, title=track.title) + new_player = await YTDLSource.from_file(track.data['filepath'], loop=self.bot.loop, title=track.title) else: url = track.data.get('webpage_url') or track.url - return await YTDLSource.from_url(url, loop=self.bot.loop, stream=True) + new_player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True) + new_player.id = track.id + new_player.plays_left = getattr(track, 'plays_left', self.get_default_plays()) + return new_player async def enqueue_track(self, guild_id: int, player, channel=None, from_web=False): if channel: self.last_channels[guild_id] = channel + if getattr(player, 'plays_left', None) is None: + player.plays_left = self.get_default_plays() + queue = self.get_queue(guild_id) queue.append(player) @@ -216,6 +223,36 @@ class MusicBot(commands.Cog): except Exception as e: print(f"Error saving config: {e}") + def get_default_plays(self) -> int: + config_path = "config/config.json" + if os.path.exists(config_path): + try: + import json + with open(config_path, "r") as f: + data = json.load(f) + val = data.get("default_plays") + if val is not None: + return int(val) + except Exception as e: + print(f"Error loading default_plays: {e}") + return 5 + + def set_default_plays(self, val: int): + config_path = "config/config.json" + try: + import json + os.makedirs(os.path.dirname(config_path), exist_ok=True) + data = {} + if os.path.exists(config_path): + with open(config_path, "r") as f: + data = json.load(f) + data["default_plays"] = int(val) + with open(config_path, "w") as f: + json.dump(data, f, indent=4) + self.default_plays = int(val) + except Exception as e: + print(f"Error saving default_plays: {e}") + def get_notification_channel(self, guild_id: int): if self.last_channels.get(guild_id): return self.last_channels[guild_id] @@ -394,7 +431,17 @@ class MusicBot(commands.Cog): player = track player.played = True - queue[next_idx] = player + current_plays = getattr(player, 'plays_left', self.get_default_plays()) + player.plays_left = current_plays - 1 + + if player.plays_left <= 0: + queue.pop(next_idx) + self.set_queue_index(guild_id, next_idx - 1) + if self.get_loop_current(guild_id): + self.set_loop_current(guild_id, False) + else: + queue[next_idx] = player + self.set_current(guild_id, player) self.add_to_history(guild_id, player) await self.trigger_state_change() @@ -445,10 +492,15 @@ class MusicBot(commands.Cog): lines = [] current_idx = self.get_queue_index(guild_id) + current_playing = self.get_current(guild_id) + if current_playing and (current_idx < 0 or current_idx >= len(queue)): + plays = getattr(current_playing, 'plays_left', 0) + lines.append(f"▶️ **{discord.utils.escape_markdown(current_playing.title)}** (obecnie odtwarzane - ostatnie odtworzenie) [🔁 {plays}]\n---") for i, item in enumerate(queue): prefix = "▶️ **" if i == current_idx else f"{i + 1}. " suffix = "** (obecnie odtwarzane)" if i == current_idx else "" - lines.append(f"{prefix}{discord.utils.escape_markdown(item.title)}{suffix}") + plays = getattr(item, 'plays_left', self.get_default_plays()) + lines.append(f"{prefix}{discord.utils.escape_markdown(item.title)}{suffix} [🔁 {plays}]") queue_list = "\n".join(lines[:30]) if len(lines) > 30: queue_list += f"\n... i {len(lines) - 30} więcej." @@ -694,6 +746,32 @@ class MusicBot(commands.Cog): ephemeral=False ) + @app_commands.command(name="defaultplays", description="Ustaw domyślną liczbę odtworzeń dla nowych utworów w kolejce") + @app_commands.describe(count="Domyślna liczba odtworzeń (1-999)") + async def default_plays_cmd(self, interaction: discord.Interaction, count: int): + if count < 1 or count > 999: + await interaction.response.send_message("❌ Liczba odtworzeń musi być z przedziału 1-999.", ephemeral=True) + return + self.set_default_plays(count) + await self.trigger_state_change() + await interaction.response.send_message(f"✅ Domyślna liczba odtworzeń dla nowych utworów została ustawiona na: **{count}**.") + + @app_commands.command(name="trackplays", description="Zmień liczbę odtworzeń dla utworu w kolejce") + @app_commands.describe(index="Pozycja w kolejce (od 1)", count="Nowa liczba odtworzeń (1-999)") + async def track_plays_cmd(self, interaction: discord.Interaction, index: int, count: int): + guild_id = interaction.guild.id + queue = self.get_queue(guild_id) + if index < 1 or index > len(queue): + await interaction.response.send_message("❌ Nieprawidłowy numer utworu w kolejce.", ephemeral=True) + return + if count < 1 or count > 999: + await interaction.response.send_message("❌ Liczba odtworzeń musi być z przedziału 1-999.", ephemeral=True) + return + track = queue[index - 1] + track.plays_left = count + await self.trigger_state_change() + await interaction.response.send_message(f"✅ Ustawiono liczbę odtworzeń dla utworu **{discord.utils.escape_markdown(track.title)}** na **{count}**.") + class MyBot(commands.Bot): def __init__(self): diff --git a/static/app.js b/static/app.js index e799d60..9f4975d 100644 --- a/static/app.js +++ b/static/app.js @@ -175,6 +175,30 @@ function bindEvents() { if (e.target === settingsModal) closeSettingsModal(); }); } + + const btnSaveDefaultPlays = document.getElementById('btn-save-default-plays'); + if (btnSaveDefaultPlays) { + btnSaveDefaultPlays.addEventListener('click', async () => { + const inputDefaultPlays = document.getElementById('setting-default-plays'); + if (!inputDefaultPlays) return; + const val = parseInt(inputDefaultPlays.value, 10); + if (isNaN(val) || val < 1 || val > 999) { + showToast('Wartość musi być z przedziału 1-999', 'error'); + return; + } + try { + const res = await fetch('/api/settings/default_plays', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ default_plays: val }) + }); + if (!res.ok) throw new Error('Failed to save setting'); + showToast(`Domyślna liczba odtworzeń: ${val}`, 'success'); + } catch (err) { + showToast('Błąd zapisu ustawienia', 'error'); + } + }); + } // Uploads Modal Events if (btnOpenAllUploads) btnOpenAllUploads.addEventListener('click', openUploadsModal); @@ -271,6 +295,7 @@ function updateProgressBar() { // Update UI States function updateUI(status) { + currentStatus = status; // 1. Connection Status & Available Channels if (status.active_voice_channel) { statusDot.className = 'status-indicator online'; @@ -320,6 +345,12 @@ function updateUI(status) { if (status.guilds) { renderSettingsGuilds(status.guilds); } + if (status.default_plays !== undefined) { + const inputDefaultPlays = document.getElementById('setting-default-plays'); + if (inputDefaultPlays && document.activeElement !== inputDefaultPlays) { + inputDefaultPlays.value = status.default_plays; + } + } // 2. Playback State if (status.current) { @@ -422,8 +453,8 @@ function updateUI(status) { } } - // 6. Queue render (if not currently dragging) - if (!isDragging) { + // 6. Queue render (if not currently dragging and not editing track plays input) + if (!isDragging && (!document.activeElement || !document.activeElement.classList.contains('track-plays-input'))) { renderQueue(status.queue, status.queue_index); renderHistory(status.history || []); } @@ -471,6 +502,13 @@ function renderQueue(queue, queueIndex = -1) { ${indexHtml}
Ustaw domyślną liczbę odtworzeń dla każdego nowo dodawanego utworu do kolejki. Przy każdym odtworzeniu utworu wartość jest pomniejszana o 1, a po spadku do 0 utwór usuwany jest z kolejki.
+Wybierz kanał tekstowy na każdym serwerze Discord, na który bot ma wysyłać powiadomienia o odtwarzanych utworach i zmianach w kolejce.
diff --git a/web.py b/web.py index 338dd94..e3c65e3 100644 --- a/web.py +++ b/web.py @@ -43,6 +43,13 @@ class TextChannelRequest(BaseModel): class PlayFileRequest(BaseModel): filename: str +class DefaultPlaysRequest(BaseModel): + default_plays: int + +class TrackPlaysRequest(BaseModel): + id: str + plays: int + # Connection Manager for WebSockets class ConnectionManager: def __init__(self): @@ -123,7 +130,8 @@ async def get_status(): "active_voice_channel": None, "text_channels": [], "active_text_channel_id": None, - "guilds": all_guilds + "guilds": all_guilds, + "default_plays": music_cog.get_default_plays() if music_cog else 5 } guild_id = guild.id @@ -172,16 +180,19 @@ async def get_status(): "url": current_track.data.get("webpage_url") or current_track.url, "id": current_track.id, "duration": current_track.duration, - "elapsed": round(current_track.elapsed, 1) + "elapsed": round(current_track.elapsed, 1), + "plays_left": getattr(current_track, 'plays_left', music_cog.get_default_plays()) } if current_track else None, "queue": [ { "title": item.title, "url": item.data.get("webpage_url") or item.url, - "id": item.id + "id": item.id, + "plays_left": getattr(item, 'plays_left', music_cog.get_default_plays()) } for item in queue ], "queue_index": music_cog.get_queue_index(guild_id), + "default_plays": music_cog.get_default_plays(), "voice_channels": all_voice_channels, "active_voice_channel": active_vc, "text_channels": text_channels, @@ -664,6 +675,40 @@ async def api_set_text_channel(req: TextChannelRequest): await broadcast_status() return {"status": "success", "channel_id": req.channel_id, "guild_id": str(guild_id)} +@app.post("/api/settings/default_plays") +async def api_set_default_plays(req: DefaultPlaysRequest): + music_cog = bot.get_cog("MusicBot") + if not music_cog: + raise HTTPException(status_code=500, detail="MusicBot cog not loaded") + if req.default_plays < 1 or req.default_plays > 999: + raise HTTPException(status_code=400, detail="Invalid default plays value") + music_cog.set_default_plays(req.default_plays) + await broadcast_status() + return {"status": "success", "default_plays": req.default_plays} + +@app.post("/api/queue/plays") +async def api_set_track_plays(req: TrackPlaysRequest): + 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") + guild_id = guild.id + queue = music_cog.get_queue(guild_id) + if req.plays < 1 or req.plays > 999: + raise HTTPException(status_code=400, detail="Invalid plays value") + found = False + for item in queue: + if item.id == req.id: + item.plays_left = req.plays + found = True + break + if not found: + raise HTTPException(status_code=404, detail="Track not found in queue") + await broadcast_status() + return {"status": "success", "id": req.id, "plays": req.plays} + # WebSocket Background Loop and Startup Hook async def status_broadcast_loop(): while True: diff --git a/ytdl.py b/ytdl.py index b9322fb..bcb1490 100644 --- a/ytdl.py +++ b/ytdl.py @@ -51,6 +51,7 @@ class YTDLSource(discord.PCMVolumeTransformer): self.id = uuid.uuid4().hex # Unique track identifier self.duration = data.get('duration') # Duration in seconds self.elapsed = 0.0 # Elapsed time in seconds + self.plays_left = data.get('plays_left', None) # Remaining plays in queue def read(self): data = super().read()