import discord from discord.ext import commands, tasks import random import os from glob import glob import time import logging import pyodbc # type: ignore import re from datetime import datetime # --- KONFIGURACJA BAZY DANYCH ACCESS --- ACCESS_DB_PATH = 'C:/Users/marek_yrlsweo/Documents/furrystatus/bot/playcount.accdb' # Użyj odpowiedniego sterownika ODBC dla Twojej wersji Office/Windows ODBC_DRIVER = '{Microsoft Access Driver (*.mdb, *.accdb)}' # Globalna zmienna na połączenie z bazą db_connection = None # --- KONFIGURACJA --- MUSIC_DIR = 'C:/Users/marek_yrlsweo/Documents/furrystatus/bot/music' # Nazwa folderu z plikami muzycznymi FILE_EXT = 'flac' # Rozszerzenie plików (możesz dodać 'ogg', 'mp3' itp.) ADS_DIR = 'C:/Users/marek_yrlsweo/Documents/furrystatus/bot/ads' # NOWA ZMIENNA: Folder z reklamami AD_CHANCE = 10 # NOWA ZMIENNA: Szansa na reklamę w % PREFIX = 'frog!' # Prefiks komend FFMPEG_EXE_PATH = r'C:/FFmpeg/bin/ffmpeg.exe' tocen = "MTM3OTA2ODIyNDk0NzA5MzUzNg.GuSMW3.OzhV0eLNLSeEZvyoEfMkXxIckHUwp_b12p8wvc" FFMPEG_OPTIONS = { # -i: Wskaźnik wejściowego strumienia (podajemy go w play_next) # -f opus: Zmusza FFmpeg do użycia kodeka Opus (zalecany przez Discorda) # -ac 2: Ustawia dwa kanały (stereo) # -ar 48000: Ustawia częstotliwość próbkowania na 48kHz (standard Discorda) # -b:a 192k: Ustawia bitrate audio na 192 kbps (najwyższy dozwolony) 'options': '-vn -filter:a "volume=0.5" -f s16le -ar 48000 -ac 2', # s16le dla PCM # before_options może zawierać argumenty specyficzne dla źródła, # ale w przypadku lokalnych plików FLAC, te domyślne są wystarczające. } # Uwaga: Discord.py automatycznie zajmie się większością tych opcji, # ale możemy wymusić bitrate, używając FFMPEG_OPUS w połączeniu z ffmpeg_before_args. # Przykładowy, bardziej agresywny zestaw dla Opus (jeśli użylibyśmy FFmpegOpusAudio): # W przypadku FFmpegPCMAudio, używamy PCM (s16le), więc bitrate jest stały. # Najważniejsze jest, abyś używał FFmpegOpusAudio, jeśli chcesz dostosować bitrate (patrz sekcja 2). # Ustawienie Intencji (Intents) intents = discord.Intents.default() intents.message_content = True intents.guilds = True intents.voice_states = True start = time.time() LOG_CHANNEL_ID = 1425511034482982943 bot = commands.Bot( command_prefix=PREFIX, intents=intents, # === KLUCZOWA ZMIANA === help_command=None # ======================== ) bot.current_track_is_ad = False bot.is_looping_queue = True # Flaga dla pętli listy (jak wcześniej) bot.is_looping_track = False # Lista plików muzycznych music_files = [] ad_files = [] HELP_PAGES = { "🎧 Muzyka": [ "`!play_loop` - Zaczyna odtwarzać muzykę w pętli (losowo/kolejno).", "`!skip` - Pomija bieżący utwór i przechodzi do następnego.", "`!repeat` - Przełącza powtarzanie obecnie odtwarzanego utworu.", "`!loop` - Przełącza pętlę listy odtwarzania (losowe utwory).", "`!leave` - Rozłącza bota, ale resetuje timer reklam." ], "📊 Statystyki": [ "`!stats` - Pokazuje TOP 10 najczęściej odtwarzanych utworów.", "`!debug` - Wyświetla informacje o statusie bota (wewnętrzne)." ], "🛠️ Administracja": [ f"`{PREFIX}help` - Wyświetla to menu pomocy." ] } PAGE_KEYS = list(HELP_PAGES.keys()) # Klasa, która tworzy i zarządza widokiem (przyciskami) class HelpView(discord.ui.View): def __init__(self, ctx, timeout=60): super().__init__(timeout=timeout) self.ctx = ctx self.current_page = 0 self.message = None # Do przechowywania wiadomości do edycji # --- FUNKCJE POMOCNICZE --- # Funkcja generująca obiekt Embed dla bieżącej strony def create_embed(self): category = PAGE_KEYS[self.current_page] description_lines = HELP_PAGES[category] embed = discord.Embed( title=f"🐸 Komendy Bota: {category}", description="\n".join(description_lines), color=discord.Color.blue() ) embed.set_footer(text=f"Strona {self.current_page + 1} z {len(PAGE_KEYS)} | Wygasa za 60s.") return embed # Funkcja aktualizująca stan przycisków def update_buttons(self): # Dezaktywuje przycisk "Wstecz" na pierwszej stronie self.children[0].disabled = (self.current_page == 0) # Dezaktywuje przycisk "Dalej" na ostatniej stronie self.children[1].disabled = (self.current_page == len(PAGE_KEYS) - 1) # --- PRZYCISKI --- # Przycisk "Wstecz" (Pierwszy przycisk w widoku) @discord.ui.button(label="⬅️ Wstecz", style=discord.ButtonStyle.secondary) async def previous_button(self, interaction: discord.Interaction, button: discord.ui.Button): # Sprawdzenie, czy kliknął autoryzowany użytkownik (ten, który użył komendy) if interaction.user != self.ctx.author: await interaction.response.send_message("❌ Tylko osoba, która wywołała tę komendę, może klikać!", ephemeral=True) return self.current_page -= 1 self.update_buttons() # Edytuj wiadomość nowym embedem i zaktualizowanymi przyciskami await interaction.response.edit_message(embed=self.create_embed(), view=self) # Przycisk "Dalej" (Drugi przycisk w widoku) @discord.ui.button(label="Dalej ➡️", style=discord.ButtonStyle.secondary) async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): if interaction.user != self.ctx.author: await interaction.response.send_message("❌ Tylko osoba, która wywołała tę komendę, może klikać!", ephemeral=True) return self.current_page += 1 self.update_buttons() await interaction.response.edit_message(embed=self.create_embed(), view=self) # Przycisk "Zamknij" (Trzeci przycisk w widoku) @discord.ui.button(label="Zakończ", style=discord.ButtonStyle.danger) async def close_button(self, interaction: discord.Interaction, button: discord.ui.Button): if interaction.user != self.ctx.author: await interaction.response.send_message("❌ Tylko osoba, która wywołała tę komendę, może klikać!", ephemeral=True) return # Usuń przyciski i zmień kolor/tytuł, aby zaznaczyć koniec sesji new_embed = self.create_embed() new_embed.title = f"🐸 Komendy Bota: Zakończono" new_embed.color = discord.Color.dark_grey() await interaction.response.edit_message(embed=new_embed, view=None) # view=None usuwa przyciski self.stop() # Zakończ oczekiwanie na timeout # Co się dzieje po wygaśnięciu czasu (timeout) async def on_timeout(self) -> None: if self.message: new_embed = self.message.embeds[0] new_embed.title = f"🐸 Komendy Bota: Sesja Wygasła" new_embed.color = discord.Color.dark_grey() # Używamy edit zamiast interaction.response.edit_message, bo nie mamy interakcji await self.message.edit(embed=new_embed, view=None) class DiscordHandler(logging.Handler): def __init__(self, bot, channel_id, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.channel_id = channel_id def emit(self, record): # Wysyłanie logów jest operacją asynchroniczną, musi być uruchomiona w pętli zdarzeń log_entry = self.format(record) # Ograniczenie długości wiadomości Discorda if len(log_entry) > 2000: log_entry = log_entry[:1990] + '...' # Utworzenie zadania do wysłania wiadomości async def send_log(): try: channel = self.bot.get_channel(self.channel_id) if channel: # Wysyłamy log w bloku kodu, aby był czytelny (Markdown) await channel.send(f'```\n{log_entry}\n```') except Exception as e: # Jeśli wystąpi błąd podczas wysyłania loga (np. brak połączenia) print(f"Błąd podczas wysyłania loga na Discorda: {e}") # Używamy bot.loop do uruchomienia asynchronicznej funkcji if self.bot.is_ready(): self.bot.loop.create_task(send_log()) def setup_logging(bot): """Konfiguruje standardowe logowanie i dodaje handler Discorda.""" # 1. Konfiguracja logowania do konsoli (jak dotąd) # Ustawiamy poziom WARNING, aby nie zalewało konsoli logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(name)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') # 2. Utworzenie instancji Handlera dla Discorda discord_handler = DiscordHandler(bot, LOG_CHANNEL_ID) # Ustawienie poziomu dla Discorda na ERROR lub CRITICAL, aby wysyłać tylko poważne błędy discord_handler.setLevel(logging.ERROR) # Ustawienie formatu dla wiadomości Discorda formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s: %(message)s') discord_handler.setFormatter(formatter) #drugi discord_handler1 = DiscordHandler(bot, LOG_CHANNEL_ID) # Ustawienie poziomu dla Discorda na ERROR lub CRITICAL, aby wysyłać tylko poważne błędy discord_handler1.setLevel(logging.INFO) # Ustawienie formatu dla wiadomości Discorda formatter1 = logging.Formatter('%(asctime)s %(levelname)s %(name)s: %(message)s') discord_handler1.setFormatter(formatter1) # 3. Dodanie Handlera do głównego loggera discord.py logger = logging.getLogger('discord') logger.addHandler(discord_handler) logger.addHandler(discord_handler1) print("Konfiguracja logowania Discord zakończona. Błędy będą wysyłane na kanał.") def get_music_files(): """Wyszukuje pliki muzyczne w folderze MUSIC_DIR.""" global music_files # Używamy glob do znalezienia wszystkich plików pasujących do wzorca # np. 'music/*.flac' music_files = glob(os.path.join(MUSIC_DIR, f'*.{FILE_EXT}')) if not music_files: print(f"Brak plików .{FILE_EXT} w folderze '{MUSIC_DIR}'. Sprawdź ścieżkę i rozszerzenie.") else: print(f"Znaleziono {len(music_files)} plików muzycznych.") def get_ad_files(): """Wyszukuje pliki reklamowe w folderze ADS_DIR.""" global ad_files # Używamy glob do znalezienia wszystkich plików pasujących do wzorca ad_files = glob(os.path.join(ADS_DIR, f'*.{FILE_EXT}')) if not ad_files: print(f"Brak plików .{FILE_EXT} w folderze '{ADS_DIR}'. Reklamy będą ignorowane.") else: print(f"Znaleziono {len(ad_files)} plików reklamowych.") def clean_track_name(file_path): # Pobierz samą nazwę pliku (np. 'Nazwa [ID].flac') file_name = os.path.basename(file_path) # 1. Usuń rozszerzenie (.flac, .mp3 itp.) name_without_ext = os.path.splitext(file_name)[0] # 2. Usuń zawartość w nawiasach kwadratowych (np. [ID]) # Używamy wyrażenia regularnego: \[.*?\] znajduje i usuwa tekst między [...] cleaned_name = re.sub(r'\s*\[.*?\]', '', name_without_ext).strip() return cleaned_name def update_play_count(file_to_play): # 1. KONFIGURACJA POŁĄCZENIA LOKALNIE conn = None conn_str = ( f'DRIVER={ODBC_DRIVER};' # Upewnij się, że ODBC_DRIVER jest zdefiniowany globalnie f'DBQ={ACCESS_DB_PATH};' # Upewnij się, że ACCESS_DB_PATH jest zdefiniowany globalnie ) try: # OTWIERANIE POŁĄCZENIA LOKALNIE conn = pyodbc.connect(conn_str) cursor = conn.cursor() # Używamy PEŁNEJ ŚCIEŻKI jako klucza w bazie danych track_key = os.path.basename(file_to_play) current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 2. LOGIKA UPDATE update_query = """ UPDATE Odtworzenia SET PlayCount = PlayCount + 1, LastPlayed = ? WHERE FilePath = ? """ cursor.execute(update_query, current_time, track_key) if cursor.rowcount == 0: # 3. LOGIKA INSERT insert_query = """ INSERT INTO Odtworzenia (FilePath, PlayCount, LastPlayed) VALUES (?, 1, ?) """ cursor.execute(insert_query, track_key, current_time) conn.commit() except pyodbc.Error as ex: print(f"Błąd podczas aktualizacji licznika: {ex}") if conn: conn.rollback() finally: # 4. ZAMYKANIE KURORA I POŁĄCZENIA W BLOKU FINALLY if 'cursor' in locals() and cursor: cursor.close() if conn: conn.close() # To zwolni plik .laccdb natychmiast! @bot.event async def on_ready(): """Wywoływane, gdy bot jest gotowy i zalogowany.""" print(f'Zalogowano jako {bot.user.name} ({bot.user.id})') # --- NOWA LINIA: KONFIGURACJA LOGÓW --- setup_logging(bot) # ------------------------------------- get_music_files() get_ad_files() if music_files: # Pętla do rozpoczęcia odtwarzania bot.loop.create_task(initial_play_start()) # Write initial status file for web UI try: write_bot_status() except Exception as e: print(f"Nie udało się zapisać bot_status.json: {e}") def write_bot_status(): """Zapisuje plik bot_status.json obok skryptu z liczbą serwerów i listą guildów.""" import json try: data = { 'guild_count': len(bot.guilds), 'guilds': [{'id': g.id, 'name': g.name} for g in bot.guilds] } base = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(base, 'bot_status.json') with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) except Exception as e: print(f"Błąd podczas zapisu bot_status.json: {e}") @bot.event async def on_guild_join(guild): # Update status file when the bot joins a guild try: write_bot_status() except Exception as e: print(f"Błąd on_guild_join write: {e}") @bot.event async def on_guild_remove(guild): # Update status file when the bot is removed from a guild try: write_bot_status() except Exception as e: print(f"Błąd on_guild_remove write: {e}") # Musimy poprawić logikę startu pętli, użyjemy prostego asynchronicznego startu async def initial_play_start(): # Czekamy chwilę, aż bot całkowicie się zaloguje i połączy await bot.wait_until_ready() # Logika do rozpoczęcia odtwarzania, jeśli już jest na kanale for guild in bot.guilds: # Znajdź voice client vc = discord.utils.get(bot.voice_clients, guild=guild) if vc and not vc.is_playing() and music_files: # Tworzenie tymczasowego obiektu Context do wywołania play_next class SimpleContext: def __init__(self, voice_client, channel): self.voice_client = voice_client self.channel = channel # Wymaga znalezienia kanału tekstowego do wysyłania wiadomości. # Użyjemy pierwszego znalezionego kanału tekstowego dla uproszczenia text_channel = discord.utils.get(guild.text_channels, name='general') or guild.text_channels[0] simple_ctx = SimpleContext(vc, text_channel) play_next(simple_ctx) break @bot.command(name='join', help='Każe botowi dołączyć do kanału głosowego, na którym jesteś.') async def join(ctx): """Bot dołącza do kanału głosowego użytkownika.""" if not ctx.author.voice: await ctx.send(f"{ctx.author.name} nie jest w kanale głosowym.") return channel = ctx.author.voice.channel # Sprawdzenie, czy bot jest już podłączony if ctx.voice_client is not None: await ctx.voice_client.move_to(channel) else: await channel.connect() await ctx.send(f"Dołączam do kanału: **{channel.name}**") @bot.command(name='leave', help='Każe botowi opuścić kanał głosowy.') async def leave(ctx): """Bot opuszcza kanał głosowy.""" if ctx.voice_client: await ctx.voice_client.disconnect() # Zatrzymanie pętli, jeśli bot opuszcza kanał if random_music_loop.is_running(): random_music_loop.stop() await ctx.send("Opuszczam kanał głosowy.") else: await ctx.send("Bot nie jest podłączony do żadnego kanału głosowego.") def play_next(ctx): vc = ctx.voice_client if not vc: print("Bot nie jest podłączony do kanału głosowego.") return # ---------------------------------------------------- # 1. LOGIKA WYBORU UTWORU # ---------------------------------------------------- file_to_play = None is_ad = False # 1.1. Tryb powtarzania pojedynczego utworu if ctx.bot.is_looping_track and ctx.bot.current_track_path: file_to_play = ctx.bot.current_track_path is_ad = ctx.bot.current_track_is_ad # Utrzymaj stan reklamy/muzyki # 1.2. Normalne odtwarzanie (lub pętla listy) else: # Losowanie, czy to będzie reklama, czy muzyka is_ad_chance = random.choices([True, False], [1, 9])[0] # 10% szans na reklamę (przykładowo) if is_ad_chance and ad_files: file_to_play = random.choice(ad_files) is_ad = True elif music_files: file_to_play = random.choice(music_files) is_ad = False else: print("Brak plików muzycznych lub reklam do odtworzenia.") return # Zapisanie stanu dla kolejnych iteracji i komend ctx.bot.current_track_is_ad = is_ad ctx.bot.current_track_path = file_to_play # Zapisz pełną ścieżkę (jako string!) # ---------------------------------------------------- # 2. MECHANIZM CALLBACK (PĘTLA) # ---------------------------------------------------- def after_playing(e): # Funkcja wywoływana, gdy utwór się skończy if e: print(f'Błąd podczas odtwarzania: {e}') return # Zmieniamy warunek kontynuacji pętli if ctx.bot.is_looping_track or ctx.bot.is_looping_queue: # Użyj vc.loop.call_soon_threadsafe do bezpiecznego wywołania z wątku after vc.loop.call_soon_threadsafe(play_next, ctx) else: print("Odtwarzanie zakończone. Bot czeka na komendę.") # ---------------------------------------------------- # 3. ROZPOCZĘCIE ODTWARZANIA # ---------------------------------------------------- if file_to_play: # 3.1. Aktualizacja statystyk (tylko dla muzyki) if not is_ad: # Pamiętaj, aby zaimplementować tę funkcję, aby używała track_name = os.path.basename(file_to_play) update_play_count(file_to_play) # 3.2. Tworzenie źródła source = discord.FFmpegOpusAudio( file_to_play, options='-b:a 192k -f opus', executable=FFMPEG_EXE_PATH ) # 3.3. Odtwarzanie! vc.play(source, after=after_playing) # 3.4. Wysyłanie powiadomienia (ASYNCHRONICZNE!) # Ponieważ jesteśmy w normalnej funkcji (nie async), musisz użyć loop.create_task: vc.loop.create_task(send_now_playing(ctx, file_to_play, is_ad)) else: print("Nie znaleziono pliku do odtworzenia.") # Wymagana funkcja do asynchronicznego wysyłania wiadomości async def send_now_playing(ctx, file_to_play, is_ad): # 1. Przygotowanie tytułów if is_ad: # Dla reklam vc_name = "🔇 Reklama" chat_title = "REKLAMA SPONSOROWANA" else: # Dla muzyki: czyszczenie nazwy i formatowanie cleaned_title = clean_track_name(file_to_play) vc_name = f"🎧 {cleaned_title}" chat_title = os.path.basename(file_to_play) # Oryginalna nazwa dla wiadomości na czacie # 2. DODANIE ZMIANY STATUSU BOTA (ACTIVITY) if is_ad: new_activity = discord.Game(name="Reklamy...") else: cleaned_title = clean_track_name(file_to_play) # Użyj ActivityType.listening dla statusu "Słuchanie..." new_activity = discord.Activity( name=cleaned_title, type=discord.ActivityType.listening ) try: # Zmiana statusu bota await ctx.bot.change_presence(activity=new_activity, status=discord.Status.online) except Exception as e: print(f"Błąd podczas zmiany statusu bota: {e}") # 3. Wysyłanie wiadomości na kanał tekstowy await ctx.send(f"▶️ Teraz odtwarzam: **{chat_title}**") @tasks.loop(seconds=5.0, count=1) # Używamy count=1, aby wykonać funkcję tylko raz async def random_music_loop(): """Zadanie w pętli do rozpoczęcia odtwarzania, jeśli jest podłączony.""" # Użyjemy tej pętli tylko do jednorazowego rozpoczęcia odtwarzania # po dołączeniu, prawdziwa pętla jest w play_next. for vc in bot.voice_clients: # Znajdź kontekst dla VoiceClient ctx = vc.guild.id if not vc.is_playing() and music_files: # Ponieważ potrzebujemy obiektu Context do wysłania wiadomości i # informacji o kanale, musimy odzyskać go. W uproszczonej wersji # przyjmiemy, że bot jest podłączony i użyjemy prostego obiektu # do wywołania play_next. class SimpleContext: def __init__(self, voice_client): self.voice_client = voice_client # Tworzymy prosty obiekt, aby przekazać go do play_next # W bardziej rozbudowanych botach, ten kawałek kodu jest lepiej # zaimplementowany w dedykowanej klasie 'MusicPlayer'. simple_ctx = SimpleContext(vc) # Rozpocznij odtwarzanie pierwszego utworu play_next(simple_ctx) break # --- KOMENDA STARTUJĄCA I ZATRZYMUJĄCA --- @bot.command(name='play_loop', help='Rozpoczyna odtwarzanie losowej muzyki w pętli.') async def play_loop(ctx): """Rozpoczyna odtwarzanie muzyki w pętli losowo.""" # 1. Sprawdzenie kanału głosowego if not ctx.author.voice: await ctx.send("Musisz być w kanale głosowym, aby użyć tej komendy!") return # 2. Dołączenie do kanału, jeśli nie jest podłączony if ctx.voice_client is None: await join(ctx) # Wywołujemy komendę join # 3. Sprawdzenie, czy są pliki if not music_files: await ctx.send(f"Brak plików .{FILE_EXT} w folderze '{MUSIC_DIR}'. Sprawdź konfigurację.") return # 4. Rozpoczęcie odtwarzania/restart if ctx.voice_client and not ctx.voice_client.is_playing(): await ctx.send("Rozpoczynam odtwarzanie losowej muzyki w pętli...") play_next(ctx) # Rozpoczęcie pierwszej piosenki, a callback zajmie się resztą elif ctx.voice_client and ctx.voice_client.is_playing(): await ctx.send("Muzyka już jest odtwarzana.") @bot.command(name='stop_loop', help='Zatrzymuje odtwarzanie i opuszcza kanał.') async def stop_loop(ctx): """Zatrzymuje odtwarzanie i opuszcza kanał.""" if ctx.voice_client: ctx.voice_client.stop() await leave(ctx) else: await ctx.send("Bot nie odtwarza muzyki.") @bot.command(name='runtime', help='Pokazuje czas działania') async def runtime(ctx): koniec = time.time() calkowity_czas_sekundy = koniec - start # Przeliczenia: godziny = int(calkowity_czas_sekundy // 3600) # Ile pełnych godzin (1h = 3600s) reszta = calkowity_czas_sekundy % 3600 minuty = int(reszta // 60) # Ile pełnych minut (1m = 60s) z reszty sekundy = reszta % 60 await ctx.send(f"działam: {godziny} godzin, {minuty} minut, {sekundy:.2f} sekund") @bot.command(name='skip', help='Pomiń muzykę na kanale głosowym') async def skip(ctx): vc = ctx.voice_client if not vc: await ctx.send("Bot nie jest w kanale głosowym.") return if not vc.is_playing(): await ctx.send("Bot nic nie odtwarza.") return # Używamy atrybutu bota do sprawdzenia stanu if ctx.bot.current_track_is_ad == True: # <--- KLUCZOWA ZMIANA await ctx.send("Gra reklama. Odtwarzanie przerwane. Proszę czekać na kolejny utwór. 📣") return # 2. Pomijanie muzyki # Jeśli nie jest reklamą, kontynuuj pomijanie if ctx.bot.current_track_is_ad == False: # <--- KLUCZOWA ZMIANA vc.stop() await ctx.send("Muzyka została pominięta. ⏩") @bot.command(name='stats', help='Pokazuje 10 najczęściej odtwarzanych utworów.') async def stats(ctx): global db_connection if not db_connection: await ctx.send("Nie można połączyć się z bazą danych, statystyki niedostępne.") return cursor = db_connection.cursor() # Zapytanie o 10 najczęściej odtwarzanych query = """ SELECT TOP 10 FilePath, PlayCount FROM Odtworzenia ORDER BY PlayCount DESC """ try: cursor.execute(query) rows = cursor.fetchall() if not rows: await ctx.send("Baza statystyk jest pusta.") return # 1. Tworzenie obiektu Embed embed = discord.Embed( title="🐸 Statystyki Odtwarzania (TOP 10)", description="Najczęściej odtwarzane utwory od startu bota.", color=discord.Color.green() ) embed.set_thumbnail(url=ctx.guild.icon.url if ctx.guild.icon else None) # Przygotowanie danych do dwóch kolumn titles = [] counts = [] # Iteracja przez wyniki i formatowanie for i, row in enumerate(rows[:10]): file_path = row.FilePath count = row.PlayCount # Użyj funkcji czyszczącej, aby wyświetlić ładny tytuł (zakładając, że masz clean_track_name) try: # Wersja bez os.path.basename, jeśli potrzebujesz tylko nazwy utworu cleaned_title = clean_track_name(file_path) except NameError: # Awaryjnie, jeśli nie masz clean_track_name cleaned_title = os.path.basename(file_path) # Wypełnianie list. Używamy kodu bloku, aby wyglądało schludniej. titles.append(f"`{i+1}.` {cleaned_title}") counts.append(f"`{count} razy`") # 2. Dodawanie pól do Embeda (dwie kolumny obok siebie) # Tytuły embed.add_field(name="Tytuł Utworu", value="\n".join(titles), inline=True) # Licznik embed.add_field(name="Odtworzeń", value="\n".join(counts), inline=True) embed.set_footer(text=f"Aktualizacja statystyk: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") # 3. Wysłanie Embeda await ctx.send(embed=embed) except pyodbc.Error as ex: # Obsługa błędów bazy danych await ctx.send(f"Wystąpił błąd podczas pobierania statystyk z bazy: {ex}") finally: cursor.close() @bot.command(name='repeat', help='Włącza/wyłącza powtarzanie obecnie odtwarzanego utworu.') async def repeat_track_toggle(ctx): vc = ctx.voice_client if vc is None or not vc.is_playing(): await ctx.send("Bot niczego nie odtwarza. Najpierw użyj komendy `!play`.") return # Sprawdzamy, czy bot ma zapamiętany jakiś utwór do powtarzania if not ctx.bot.current_track_path: await ctx.send("Nie zapamiętano ścieżki utworu. Najpierw odtwórz coś komendą `!play`.") return # Przełącz stan ctx.bot.is_looping_track = not ctx.bot.is_looping_track if ctx.bot.is_looping_track: # Jeśli włączamy powtarzanie utworu, wyłączamy pętlę listy, by uniknąć kolizji ctx.bot.is_looping_queue = False current_name = os.path.basename(ctx.bot.current_track_path) await ctx.send(f"🔁 **Włączono powtarzanie**! Utwór **{current_name}** będzie odtwarzany w pętli.") else: # Po wyłączeniu, przywracamy pętlę listy jako domyślny tryb ctx.bot.is_looping_queue = True await ctx.send("❌ **Wyłączono powtarzanie.** Bot powrócił do odtwarzania listy.") @bot.command(name='loop', help='Włącza/wyłącza automatyczne odtwarzanie w pętli (z listy).') async def loop_toggle(ctx): ctx.bot.is_looping_queue = not ctx.bot.is_looping_queue if ctx.bot.is_looping_queue: ctx.bot.is_looping_track = False # Upewnij się, że powtarzanie utworu jest wyłączone await ctx.send("🔄 **Włączono** automatyczne odtwarzanie w pętli. Muzyka będzie grała bez przerwy.") else: await ctx.send("❌ **Wyłączono** automatyczne odtwarzanie. Po zakończeniu utworu bot się zatrzyma.") @bot.command(name='help', help='Wyświetla interaktywne menu pomocy z podziałem na strony.') async def help_command(ctx): # 1. Utwórz instancję widoku (kontrolera przycisków) view = HelpView(ctx) # 2. Ustaw stan przycisków dla pierwszej strony (Wstecz jest wyłączony) view.update_buttons() # 3. Wyślij wiadomość z pierwszym embedem i przyciskami message = await ctx.send(embed=view.create_embed(), view=view) # 4. Zapisz wiadomość w widoku, aby móc ją edytować w on_timeout view.message = message # Uruchomienie bota bot.run('MTM3OTA2ODIyNDk0NzA5MzUzNg.GuSMW3.OzhV0eLNLSeEZvyoEfMkXxIckHUwp_b12p8wvc') # Replace with your own token.