diff --git a/bot.py b/bot.py index e672f76..37f16a2 100644 --- a/bot.py +++ b/bot.py @@ -135,7 +135,70 @@ class MusicBot(commands.Cog): self.last_channels[guild_id] = interaction.channel try: - player = await YTDLSource.from_url(query, loop=self.bot.loop, stream=True) + filepath = os.path.join("uploads", os.path.basename(query)) + if os.path.exists(filepath) and not query.startswith("http"): + player = await YTDLSource.from_file(filepath, loop=self.bot.loop) + else: + player = await YTDLSource.from_url(query, loop=self.bot.loop, stream=True) + + if interaction.guild.voice_client.is_playing(): + self.get_queue(guild_id).append(player) + await self.trigger_state_change() + await interaction.followup.send( + f"Dodano do kolejki: {discord.utils.escape_markdown(player.title)}", + ephemeral=False + ) + else: + self.set_current(guild_id, player) + await self.trigger_state_change() + interaction.guild.voice_client.play( + player, + after=lambda e: asyncio.run_coroutine_threadsafe( + self.play_next(guild_id), + self.bot.loop + ) + ) + await interaction.followup.send( + f"Teraz odtwarzane: {discord.utils.escape_markdown(player.title)}", + ephemeral=False + ) + except Exception as e: + await interaction.followup.send( + f"Wystąpił błąd: {e}", + ephemeral=True + ) + + async def uploads_autocomplete( + self, + interaction: discord.Interaction, + current: str, + ) -> list[app_commands.Choice[str]]: + uploads_dir = "uploads" + if not os.path.exists(uploads_dir): + return [] + files = [f for f in os.listdir(uploads_dir) if os.path.isfile(os.path.join(uploads_dir, f)) and not f.startswith(".")] + filtered = [f for f in files if current.lower() in f.lower()][:25] + return [app_commands.Choice(name=f, value=f) for f in filtered] + + @app_commands.command(name="playfile", description="Odtwórz plik przesłany na serwer") + @app_commands.describe(filename="Wybierz plik z listy przesłanych") + @app_commands.autocomplete(filename=uploads_autocomplete) + async def playfile(self, interaction: discord.Interaction, filename: str): + """Odtwórz plik przesłany na serwer""" + if not await self.ensure_voice(interaction): + return + + await interaction.response.defer() + guild_id = interaction.guild.id + self.last_channels[guild_id] = interaction.channel + + filepath = os.path.join("uploads", os.path.basename(filename)) + if not os.path.exists(filepath): + await interaction.followup.send("Nie znaleziono pliku na serwerze.", ephemeral=True) + return + + try: + player = await YTDLSource.from_file(filepath, loop=self.bot.loop) if interaction.guild.voice_client.is_playing(): self.get_queue(guild_id).append(player) @@ -175,8 +238,11 @@ class MusicBot(commands.Cog): if loop_current and current: try: - url = current.data.get('webpage_url') or current.url - player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True) + if current.data.get('is_local'): + 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 + player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True) self.set_current(guild_id, player) await self.trigger_state_change() voice_client.play( diff --git a/compose.yaml b/compose.yaml index cb9419d..bec4dd0 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,4 +10,5 @@ services: - DISCORD_TOKEN=${DISCORD_TOKEN} volumes: - ./config:/app/config + - ./uploads:/app/uploads restart: unless-stopped diff --git a/static/app.js b/static/app.js index 68c50a8..eb0a99c 100644 --- a/static/app.js +++ b/static/app.js @@ -42,6 +42,14 @@ const queueList = document.getElementById('queue-list'); const emptyQueueMsg = document.getElementById('empty-queue-msg'); const queueCountBadge = document.getElementById('queue-count'); const toastContainer = document.getElementById('toast-container'); +const fileInput = document.getElementById('file-input'); +const btnSelectFile = document.getElementById('btn-select-file'); +const uploadProgressContainer = document.getElementById('upload-progress-container'); +const uploadProgressFill = document.getElementById('upload-progress-fill'); +const uploadStatusText = document.getElementById('upload-status-text'); +const uploadsList = document.getElementById('uploads-list'); +const emptyUploadsMsg = document.getElementById('empty-uploads-msg'); +const uploadsCountBadge = document.getElementById('uploads-count'); // Progress Bar DOM Elements const progressContainer = document.getElementById('progress-container'); @@ -100,6 +108,7 @@ function connectWebSocket() { document.addEventListener('DOMContentLoaded', () => { // Connect to WebSocket instead of polling connectWebSocket(); + fetchUploads(); // Start Smooth Progress Tracker (every 100ms) setInterval(() => { @@ -148,6 +157,12 @@ function bindEvents() { // Text Channel Select textChannelSelect.addEventListener('change', changeTextChannel); + + // Upload Events + if (btnSelectFile && fileInput) { + btnSelectFile.addEventListener('click', () => fileInput.click()); + fileInput.addEventListener('change', handleFileUpload); + } } // Show Toast Notification @@ -719,3 +734,131 @@ async function changeTextChannel() { showToast('Nie udało się zapisać kanału powiadomień', 'error'); } } + +// Fetch Uploaded Files +async function fetchUploads() { + try { + const res = await fetch('/api/uploads'); + if (!res.ok) return; + const data = await res.json(); + renderUploads(data.files || []); + } catch (err) { + console.error('Błąd pobierania plików:', err); + } +} + +// Render Uploaded Files +function renderUploads(files) { + if (!uploadsList) return; + const items = uploadsList.querySelectorAll('.queue-item'); + items.forEach(el => el.remove()); + + if (!files || files.length === 0) { + emptyUploadsMsg.style.display = 'flex'; + uploadsCountBadge.textContent = '0 plików'; + return; + } + + emptyUploadsMsg.style.display = 'none'; + uploadsCountBadge.textContent = `${files.length} ${files.length === 1 ? 'plik' : files.length < 5 ? 'pliki' : 'plików'}`; + + files.forEach(file => { + const li = document.createElement('li'); + li.className = 'queue-item'; + + const safeFilename = file.filename.replace(/'/g, "\\'"); + + li.innerHTML = ` +