742 lines
29 KiB
Python
742 lines
29 KiB
Python
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. |