From 1984bb0f98ff12a716580f776ecf028063ff9d28 Mon Sep 17 00:00:00 2001 From: Javi <45560967+javdc@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:00:53 +0100 Subject: [PATCH 1/3] [WIP] Different queue for each voice channel --- dalle.py | 17 +++-- guild_queue.py | 146 ++++++++++++++++++++++++++++++++++++++ main.py | 187 ++++++++++++++----------------------------------- text.py | 9 --- threads.py | 3 +- tts.py | 13 ++-- utils.py | 14 ++++ voice.py | 54 ++++++-------- 8 files changed, 251 insertions(+), 192 deletions(-) create mode 100644 guild_queue.py delete mode 100644 text.py diff --git a/dalle.py b/dalle.py index 7120d1c..bdd734a 100644 --- a/dalle.py +++ b/dalle.py @@ -8,6 +8,7 @@ import requests from PIL import Image +from discord.abc import Messageable DALLE_FOLDER_PATH = "./dalle" @@ -18,9 +19,10 @@ class ResponseType(Enum): class DalleImages: - def __init__(self, response_type: ResponseType, image: Optional[str]): + def __init__(self, response_type: ResponseType, image: Optional[str], messageable: Messageable): self.__response_type = response_type self.__image = image + self.__messageable = messageable def get_response_type(self) -> ResponseType: return self.__response_type @@ -28,6 +30,9 @@ def get_response_type(self) -> ResponseType: def get_image(self) -> Optional[str]: return self.__image + def get_messageable(self) -> Messageable: + return self.__messageable + def check_dalle_dir(): if not os.path.exists(DALLE_FOLDER_PATH): @@ -49,7 +54,7 @@ def remove_image_from_memory(image_name: str): # Doing a similar image proccesing as in https://github.com/borisdayma/dalle-mini/blob/main/app/gradio/backend.py -def generate_images(text: str, listener: Callable[[DalleImages], Any]): +def generate_images(text: str, listener: Callable[[DalleImages], Any], messageable: Messageable): try: url = "https://bf.dallemini.ai/generate" data = {"prompt": text} @@ -61,19 +66,19 @@ def generate_images(text: str, listener: Callable[[DalleImages], Any]): images = json["images"] images = [Image.open(BytesIO(base64.b64decode(img.replace("\\n", "\n")))) for img in images] check_dalle_dir() - result = DalleImages(ResponseType.SUCCESS, generate_image_collage(images)) + result = DalleImages(ResponseType.SUCCESS, generate_image_collage(images), messageable) listener(result) elif response.status_code == 503: log.warning("generate_images >> Servicio 503, intentando de nuevo...") - generate_images(text, listener) + generate_images(text, listener, messageable) else: - listener(DalleImages(ResponseType.FAILURE, None)) + listener(DalleImages(ResponseType.FAILURE, None, messageable)) except Exception as e: log.error(f"generate_images >> Exception: {str(e)}") - listener(DalleImages(ResponseType.FAILURE, None)) + listener(DalleImages(ResponseType.FAILURE, None, messageable)) def generate_image_collage(images: list[Image]) -> str: diff --git a/guild_queue.py b/guild_queue.py new file mode 100644 index 0000000..ff0015f --- /dev/null +++ b/guild_queue.py @@ -0,0 +1,146 @@ +import logging as log +from typing import Optional + +from discord import VoiceClient +from discord.abc import Messageable +from discord.channel import VocalGuildChannel +from discord.ext import tasks + +from utils import remove_files +from voice import Sound, stop_and_disconnect, SoundType, play_sound + + +class GuildQueue: + def __init__(self, guild_id: int, vocal_channel: VocalGuildChannel, messageable: Messageable, sound_queue: list[Sound]): + self.__guild_id = guild_id + self.__vocal_channel = vocal_channel + self.__messageable = messageable + self.__sound_queue = sound_queue + self.__voice_client = None + self.__finished = False + self.__pending_files_for_deletion = [] + + def get_guild_id(self) -> int: + return self.__guild_id + + def get_vocal_channel(self) -> VocalGuildChannel: + return self.__vocal_channel + + def get_messageable(self) -> Messageable: + return self.__messageable + + def get_sound_queue(self) -> list[Sound]: + return self.__sound_queue + + def add_sound_to_queue(self, sound: Sound): + self.__sound_queue.append(sound) + + def clear_queue(self): + self.__sound_queue.clear() + + def get_voice_client(self) -> Optional[VoiceClient]: + return self.__voice_client + + async def get_or_connect_to_voice_client(self) -> Optional[VoiceClient]: + if self.__voice_client is None: + self.__voice_client = await self.__vocal_channel.connect() + return self.__voice_client + + def has_finished(self) -> bool: + return self.__finished + + def set_finished(self, ready_for_removal: bool): + self.__finished = ready_for_removal + + def add_pending_file_for_deletion(self, path: str): + self.__pending_files_for_deletion.append(path) + + def get_pending_files_for_deletion(self) -> list[str]: + return self.__pending_files_for_deletion + + +guild_queues: list[GuildQueue] = [] + + +def get_guild_queue(guild_id: int) -> Optional[GuildQueue]: + return next((gq for gq in guild_queues if gq.get_guild_id() == guild_id), None) + + +def add_new_guild_queue(guild_id: int, vocal_channel: VocalGuildChannel, messageable: Messageable) -> Optional[GuildQueue]: + guild_queue = GuildQueue(guild_id, vocal_channel, messageable, []) + if guild_queue is not None: + guild_queues.append(guild_queue) + return guild_queue + + +async def cleanup_guild_queues(): + not_finished_guild_queues = [] + for guild_queue in guild_queues: + if not guild_queue.has_finished(): + not_finished_guild_queues.append(guild_queue) + else: + await stop_and_disconnect(guild_queue.get_voice_client()) + remove_files(guild_queue.get_pending_files_for_deletion()) + + guild_queues.clear() + guild_queues.extend(not_finished_guild_queues) + if len(guild_queues) == 0: + queue_vitals.stop() + + +async def add_to_queue(guild_id: int, vocal_channel: VocalGuildChannel, messageable: Messageable, sound: Sound): + try: + guild_queue = get_guild_queue(guild_id) + should_start_vitals = False + + if guild_queue is None: + guild_queue = add_new_guild_queue(guild_id, vocal_channel, messageable) + should_start_vitals = True + else: + await messageable.send(f":notes: Añadido a la cola `{sound.get_name()}`.") + + if guild_queue is not None: + guild_queue.add_sound_to_queue(sound) + if should_start_vitals and not queue_vitals.is_running(): + queue_vitals.start() # TODO doesn't work when method is run from another thread + + except Exception as e: + log.error("add_to_queue >> Exception thrown when adding sound to queue.", exc_info=e) + + +async def play_sounds_in_all_queues(): + for guild_queue in guild_queues: + try: + voice_client = await guild_queue.get_or_connect_to_voice_client() + if isinstance(voice_client, VoiceClient) and not voice_client.is_playing(): + sound_queue = guild_queue.get_sound_queue() + if len(sound_queue) == 0: + guild_queue.set_finished(True) + + else: + sound = sound_queue[0] + + if sound.get_sound_type() is SoundType.TTS: + await guild_queue.get_messageable().send(f":microphone: Reproduciendo un mensaje tts en `{voice_client.channel.name}`.") + guild_queue.add_pending_file_for_deletion(sound.get_source()) + elif sound.get_sound_type() is not SoundType.FILE_SILENT: + await guild_queue.get_messageable().send(f":notes: Reproduciendo `{sound.get_name()}` en `{voice_client.channel.name}`.") + + await play_sound(voice_client, sound) + sound_queue.pop(0) + + except Exception as e: + log.error("play_sounds_in_all_queues >> Caught exception playing sound. Removing sound queue for this guild...", exc_info=e) + sound_queue = guild_queue.get_sound_queue() + if len(sound_queue) > 0: + removed_sound = guild_queue.get_sound_queue().pop(0) + if removed_sound.get_sound_type() is SoundType.TTS: + guild_queue.add_pending_file_for_deletion(removed_sound.get_source()) + if len(sound_queue) == 0: + guild_queue.set_finished(True) + + +@tasks.loop(seconds=1, reconnect=True) +async def queue_vitals(): + await play_sounds_in_all_queues() + await cleanup_guild_queues() diff --git a/main.py b/main.py index 6c7701d..ebebb65 100644 --- a/main.py +++ b/main.py @@ -4,20 +4,22 @@ from threading import Event import discord -from discord import Message, Guild +from discord import Guild +from discord.abc import Messageable +from discord.channel import VocalGuildChannel from discord.ext import commands from discord.ext import tasks -from bd import Database -from dalle import ResponseType, generate_images, clear_dalle, remove_image_from_memory, DalleImages from ai import * -from text import TextChannel +from bd import Database +from dalle import ResponseType, generate_images, clear_dalle, remove_image_from_memory, DalleImages +from guild_queue import add_to_queue, get_guild_queue +from processors import process_link, process_reactions from threads import launch -from tts import generate_tts, clear_tts, tts_base_url +from tts import generate_tts, tts_base_url from utils import * from voice import * from youtube import * -from processors import process_link, process_reactions MAX_RESPONSE_CHARACTERS = 2000 - 6 @@ -26,17 +28,14 @@ bot = commands.Bot(command_prefix='!', intents=intents, guild_subscriptions=True, fetch_offline_members=True) bot.remove_command("help") -sound_queue: list[Sound] = [] dalle_results_queue: list[DalleImages] = [] max_number = 10000 kiwi_chance = 500 dalle_event = Event() -tts_event = Event() @bot.event async def on_ready(): - log.info('{0.user} is alive!'.format(bot)) if is_debug_mode(): log.getLogger().setLevel(log.DEBUG) log.debug(">> Debug mode is ON") @@ -47,6 +46,7 @@ async def on_ready(): await bot.change_presence(activity=discord.Game("~bip-bop")) kiwi.start() + log.info(f"{bot.user} is alive!") sdos = bot.get_guild(689108711452442667) if sdos is not None: await sdos.me.edit(nick="Fran López") @@ -80,9 +80,7 @@ async def on_command_error(ctx: Context, exception: Exception): @bot.event async def on_error(event_name: str, *args, **kwargs): - if channel_text.get_text_channel() is not None: - await channel_text.get_text_channel().send(f"Ha ocurrido un error con {event_name}, {args}, {kwargs}.") - log.error(args) + log.error(f"on_error >> Error on event {event_name}, {args}, {kwargs}") @bot.event @@ -160,59 +158,64 @@ async def search(ctx: Context, arg: str): @bot.command(aliases=["p"], require_var_positional=True) async def play(ctx: Context, *args: str): - user_voice_channel = get_user_voice_channel(ctx) - if user_voice_channel is not None: + if await audio_play_prechecks(ctx.message): async for sound in generate_sounds(ctx, *args): database.register_user_interaction_play_sound(ctx.author.name, sound) - await add_to_queue(ctx, user_voice_channel, sound) - else: - await ctx.send("No estás en ningún canal conectado. :confused:") + await add_to_queue(ctx.guild.id, ctx.author.voice.channel, ctx.channel, sound) @bot.command(aliases=["decir", "t", "say"], require_var_positional=True) async def tts(ctx: Context, *args: str): text = " ".join(args) - user_voice_channel = get_user_voice_channel(ctx) - database.register_user_interaction(ctx.author.name, "tts") - - if user_voice_channel is not None: - channel_text.set_text_channel(ctx.channel) - voice_channel.set_voice_channel(user_voice_channel) - - await ctx.send(":tools::snail: Generando mensaje tts...") - launch(lambda: generate_tts(text, tts_listener)) + if await audio_play_prechecks(ctx.message): + await ctx.send(":tools::snail: Generando mensaje tts...") + database.register_user_interaction(ctx.author.name, "tts") + launch(lambda: generate_tts(text, ctx.guild.id, ctx.author.voice.channel, ctx.channel, tts_listener)) @bot.command(aliases=["q", "cola"]) async def queue(ctx: Context): - embed_msg = discord.Embed(title="Cola de sonidos", description=f"Actualmente hay {len(sound_queue)} sonidos en la cola.", color=0x01B05B) - database.register_user_interaction(ctx.author.name, "queue") + guild = ctx.guild + if guild is not None: + guild_queue = get_guild_queue(guild.id) + sound_queue = guild_queue.get_sound_queue() if guild_queue is not None else [] - if len(sound_queue) > 0: - sounds = map(lambda sound: sound.get_name(), sound_queue) - embed_msg.add_field(name="Sonidos en cola", value=", ".join(sounds), inline=False) + embed_msg = discord.Embed(title="Cola de sonidos", description=f"Actualmente hay {len(sound_queue)} sonidos en la cola de {guild.name}.", color=0x01B05B) + database.register_user_interaction(ctx.author.name, "queue") - await ctx.send(embed=embed_msg) + if len(sound_queue) > 0: + sounds = map(lambda sound: sound.get_name(), sound_queue) + embed_msg.add_field(name="Sonidos en cola", value=", ".join(sounds), inline=False) + + await ctx.send(embed=embed_msg) + else: + await ctx.send("No estás conectado a un servidor :angry:") @bot.command(aliases=["s"]) async def stop(ctx: Context): - for voice_client in bot.voice_clients: - if isinstance(voice_client, VoiceClient) and voice_client.guild == ctx.guild and voice_client.is_playing(): + guild = ctx.guild + if guild is not None: + voice_client = guild.voice_client + if isinstance(voice_client, VoiceClient): database.register_user_interaction(ctx.author.name, "stop") voice_client.stop() await ctx.send(":stop_button: Sonido parado.") - break + else: + await ctx.send("No estás conectado a un servidor :angry:") @bot.command(aliases=["dc"]) async def disconnect(ctx: Context): - for voice_client in bot.voice_clients: - if isinstance(voice_client, VoiceClient) and voice_client.guild == ctx.guild: + guild = ctx.guild + if guild is not None: + voice_client = guild.voice_client + if isinstance(voice_client, VoiceClient): database.register_user_interaction(ctx.author.name, "disconnect") - await clear_bot(voice_client) + await stop_and_disconnect(voice_client) await ctx.send(":robot: Desconectando...") - break + else: + await ctx.send("No estás conectado a un servidor :angry:") @bot.command(aliases=["e", "encuesta"], require_var_positional=True) @@ -244,28 +247,23 @@ async def ask(ctx: Context, *, text: str = ""): @bot.command(aliases=["yt"], require_var_positional=True) async def youtube(ctx: Context, *args: str): search_query = " ".join(args) - user_voice_channel = get_user_voice_channel(ctx) database.register_user_interaction(ctx.author.name, "youtube") - if user_voice_channel is not None: - channel_text.set_text_channel(ctx.channel) + if await audio_play_prechecks(ctx.message): await ctx.send(f":clock10: Buscando `{search_query}` en YouTube...") yt_dlp_info = yt_search_and_extract_yt_dlp_info(search_query) if yt_dlp_info is not None: async for sound in generate_sounds_from_yt_dlp_info(ctx, yt_dlp_info): - await add_to_queue(ctx, user_voice_channel, sound) + await add_to_queue(ctx.guild.id, ctx.author.voice.channel, ctx.channel, sound) else: await ctx.send(":no_entry_sign: No se ha encontrado ningún contenido.") - else: - await ctx.send("No estás en ningún canal conectado. :confused:") @bot.command(aliases=["d"], require_var_positional=True) async def dalle(ctx: Context, *args: str): text = " ".join(args) database.register_user_interaction(ctx.author.name, "dalle") - channel_text.set_text_channel(ctx.channel) await ctx.send(":clock10: Generando imagen. Puede tardar varios minutos...") - launch(lambda: generate_images(text, dalle_listener)) + launch(lambda: generate_images(text, dalle_listener, ctx.channel)) @bot.command(aliases=["co"]) @@ -296,68 +294,15 @@ async def youtubemusic(ctx: Context, *args: str): await ctx.send(":no_entry_sign: No se ha encontrado ningún contenido.") -async def add_to_queue(ctx: Context, user_voice_channel: Optional[VoiceChannel], sound: Sound): - if user_voice_channel is not None: - channel_text.set_text_channel(ctx.channel) - if voice_channel.get_voice_client() is None: - client = await user_voice_channel.connect() - voice_channel.set_voice_client(client) - sound_queue.append(sound) - bot_vitals.start() - - else: - await ctx.send(f":notes: Añadido a la cola `{sound.get_name()}`.") - sound_queue.append(sound) - - def dalle_listener(result: DalleImages): dalle_results_queue.append(result) dalle_event.set() -def tts_listener(original_file: str): +def tts_listener(guild_id: int, vocal_channel: VocalGuildChannel, messageable: Messageable, original_file: str): filename = original_file.replace(tts_base_url, "").replace(".mp3", "") sound = Sound(filename, SoundType.TTS, original_file) - sound_queue.append(sound) - tts_event.set() - - -@tasks.loop(seconds=1, reconnect=True) -async def bot_vitals(): - if voice_channel.get_voice_client() is None and voice_channel.get_voice_channel() is not None: - log.info("bot_vitals >> No hay ningún cliente conectado") - voice_client = await get_voice_client(voice_channel) - log.info(f"bot_vitals >> Conectando a {voice_client}...") - voice_channel.set_voice_client(voice_client) - - try: - for voice_client in bot.voice_clients: - if isinstance(voice_client, VoiceClient) and not voice_client.is_playing(): - if len(sound_queue) == 0: - await clear_bot(voice_client) - - else: - sound = sound_queue[0] - - if sound.get_sound_type() == SoundType.FILE_SILENT: - pass - elif sound.get_sound_type() == SoundType.TTS: - await channel_text.get_text_channel().send(f":microphone: Reproduciendo un mensaje tts en `{voice_client.channel.name}`.") - else: - await channel_text.get_text_channel().send(f":notes: Reproduciendo `{sound.get_name()}` en `{voice_client.channel.name}`.") - - await play_sound(voice_client, sound) - sound_queue.pop(0) - break - - else: - log.info("bot_vitals >> Parece que se ha cerrado la conexión de manera inesperada, limpiando la cola...") - await clear_bot(None) - - except Exception: - log.warning("bot_vitals >> Something happened, stopping bot_vitals.") - traceback.print_exc() - await clear_bot(None) + add_to_queue(guild_id, vocal_channel, messageable, sound) # TODO await @tasks.loop(minutes=1) @@ -392,13 +337,11 @@ async def kiwi(): sound_name = "ohvaya" if sound_name is not None: - voice_client = await eci_channel.connect() - voice_channel.set_voice_client(voice_client) - sound = Sound(sound_name, SoundType.FILE_SILENT, generate_audio_path(sound_name)) - log.info(f"kiwi >> Playing {sound_name}") database.register_user_interaction("kiwi", "kiwi", sound_name) - sound_queue.append(sound) - bot_vitals.start() + android_channel_messageable = bot.get_partial_messageable(857572212935360512) + sound = Sound(sound_name, SoundType.FILE_SILENT, generate_audio_path(sound_name)) + await add_to_queue(eci_channel.guild.id, eci_channel, android_channel_messageable, sound) + log.info(f"kiwi >> Added {sound_name} to queue") except discord.errors.ClientException as exc: log.error(">> Exception captured. Something happened at kiwi()", exc_info=exc) @@ -410,12 +353,12 @@ async def dalle_vitals(): log.info(f"dale_vitals >> Hay imágenes en la cola: {len(dalle_results_queue)} imágenes") if result.get_response_type() == ResponseType.SUCCESS: with open(result.get_image(), "rb") as image_file: - await channel_text.get_text_channel().send(":e_mail: Imagen recibida:", file=discord.File(image_file, filename="dalle.png")) + await result.get_messageable().send(":e_mail: Imagen recibida:", file=discord.File(image_file, filename="dalle.png")) remove_image_from_memory(result.get_image()) else: - await channel_text.get_text_channel().send(":confused: Ha ocurrido un error generando la imagen. Intenta de nuevo.") + await result.get_messageable().send(":confused: Ha ocurrido un error generando la imagen. Intenta de nuevo.") dalle_results_queue.remove(result) else: @@ -430,32 +373,8 @@ async def event_listener(): if not dalle_vitals.is_running(): dalle_vitals.start() - if tts_event.is_set(): - tts_event.clear() - if not bot_vitals.is_running(): - bot_vitals.start() - - -async def clear_bot(voice_client: Optional[VoiceClient]): - try: - if voice_client is not None: - voice_client.stop() - await voice_client.disconnect() - - except Exception: - log.warning(">> Exception captured. voice_client wasn't connected or something happened at clear_bot()") - traceback.print_exc() - - voice_channel.set_voice_client(None) - voice_channel.set_voice_channel(None) - bot_vitals.stop() - sound_queue.clear() - clear_tts() - if __name__ == "__main__": - channel_text = TextChannel() - voice_channel = CurrentVoiceChannel() database = Database(get_username_key(), get_password_key(), get_database_key()) openai_client = OpenAiClient(get_openai_key()) bot.run(get_bot_key()) diff --git a/text.py b/text.py deleted file mode 100644 index f056715..0000000 --- a/text.py +++ /dev/null @@ -1,9 +0,0 @@ -class TextChannel: - def __init__(self): - self.text_channel = None - - def get_text_channel(self): - return self.text_channel - - def set_text_channel(self, text_channel): - self.text_channel = text_channel diff --git a/threads.py b/threads.py index 86ec99d..7adf745 100644 --- a/threads.py +++ b/threads.py @@ -1,5 +1,6 @@ -from threading import Thread import logging as log +from threading import Thread + def launch(suspend_fun): try: diff --git a/tts.py b/tts.py index ed18dcf..3019007 100644 --- a/tts.py +++ b/tts.py @@ -8,6 +8,8 @@ from urllib.request import urlretrieve import ffmpy +from discord.abc import Messageable +from discord.channel import VocalGuildChannel from gtts import gTTS tts_base_url = "./tts/" @@ -18,12 +20,6 @@ def check_base_dir(): os.makedirs(tts_base_url) -def clear_tts(): - check_base_dir() - for file in os.listdir(tts_base_url): - os.remove(os.path.join(tts_base_url, file)) - - def get_loquendo_tts(text: str) -> Optional[str]: try: url_encoded_text = quote(text) @@ -52,14 +48,14 @@ def get_google_tts(text: str) -> str: return file -def generate_tts(text: str, listener: Callable[[str], Any]): +def generate_tts(text: str, guild_id: int, vocal_channel: VocalGuildChannel, messageable: Messageable, listener: Callable[[int, VocalGuildChannel, Messageable, str], Any]): if len(text) > 600: audio = get_google_tts(text) else: audio = get_loquendo_tts(text) - listener(audio) + listener(guild_id, vocal_channel, messageable, audio) def get_speed(text: str) -> float: @@ -80,6 +76,7 @@ def change_speed(file_name: str, speed: float) -> str: new_file_name = f"{tts_base_url}tts_{str(time.time())}.mp3" ff = ffmpy.FFmpeg(inputs={file_name: None}, outputs={new_file_name: f"-filter:a atempo={speed}"}) ff.run() + # TODO try this remove_file(file_name) return new_file_name except Exception: diff --git a/utils.py b/utils.py index d2c4cc7..0e37029 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,6 @@ import json import os +import traceback AUDIO_FOLDER_PATH = "./audio" @@ -78,5 +79,18 @@ def generate_sound_list_format(sounds: list[str]) -> list[list[str]]: return list_sounds +def remove_file(path: str): + try: + if os.path.exists(path): + os.remove(path) + except Exception: + traceback.print_exc() + + +def remove_files(paths: list[str]): + for path in paths: + remove_file(path) + + if __name__ == "__main__": pass diff --git a/voice.py b/voice.py index cb2426f..07f40a3 100644 --- a/voice.py +++ b/voice.py @@ -2,14 +2,13 @@ import os import traceback from collections.abc import AsyncIterable -from typing import Optional, Any +from enum import Enum +from typing import Optional, Any, Union -from discord import FFmpegPCMAudio, VoiceClient, VoiceChannel +from discord import FFmpegPCMAudio, VoiceClient, Member, User, Message from discord.ext.commands import Context from utils import AUDIO_FOLDER_PATH -from enum import Enum - from youtube import is_suitable_for_yt_dlp, extract_yt_dlp_info FFMPEG_OPTIONS_FOR_REMOTE_URL = { @@ -18,24 +17,6 @@ } -class CurrentVoiceChannel: - def __init__(self): - self.__voice_channel = None - self.__voice_client = None - - def get_voice_channel(self) -> Optional[VoiceChannel]: - return self.__voice_channel - - def set_voice_channel(self, voice_channel: Optional[VoiceChannel]): - self.__voice_channel = voice_channel - - def get_voice_client(self) -> Optional[VoiceClient]: - return self.__voice_client - - def set_voice_client(self, voice_client: Optional[VoiceClient]): - self.__voice_client = voice_client - - class SoundType(Enum): FILE = 0 FILE_SILENT = 1 @@ -59,6 +40,18 @@ def get_sound_type(self) -> SoundType: return self.__sound_type +async def audio_play_prechecks(message: Message) -> bool: + if message.guild is None or not isinstance(message.author, Member): + await message.channel.send("No estás conectado a un servidor :angry:") + return False + + if message.author.voice is None or message.author.voice.channel is None: + await message.channel.send("No estás en ningún canal conectado :confused:") + return False + + return True + + def generate_audio_path(name: str) -> str: return f"{AUDIO_FOLDER_PATH}/{name}.mp3" @@ -75,21 +68,14 @@ async def play_sound(client: Optional[VoiceClient], sound: Optional[Sound]): client.play(source=get_audio(sound)) -async def get_voice_client(voice_channel: CurrentVoiceChannel) -> VoiceClient: - if voice_channel.get_voice_client() is not None: - return voice_channel.get_voice_client() - - else: - return await voice_channel.get_voice_channel().connect() - - -def get_user_voice_channel(ctx: Context): +async def stop_and_disconnect(voice_client: Optional[VoiceClient]): try: - voice_state = ctx.message.author.voice - return voice_state.channel if voice_state is not None else None + if voice_client is not None: + voice_client.stop() + await voice_client.disconnect() except Exception: - log.error("get_user_voice_channel >> Exception thrown when getting voice channel from context.") + log.error("stop_and_disconnect >> Exception captured. voice_client wasn't connected or something happened.") traceback.print_exc() From 811f6d156bca6283a176d004acc0143ef65c61db Mon Sep 17 00:00:00 2001 From: Javi <45560967+javdc@users.noreply.github.com> Date: Wed, 13 Mar 2024 18:42:58 +0100 Subject: [PATCH 2/3] Finish different queue for each voice channel --- README.md | 6 +++--- guild_queue.py | 19 +++++------------- main.py | 9 ++++----- tts.py | 52 ++++++++++++++++++++++++++------------------------ utils.py | 14 ++++++++++++++ 5 files changed, 53 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index d7122f6..51ce252 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ ECIBot is a Discord bot made in Python. It's main purpose is to be a custom bot * `!confetti `: Plays the specified number of random Confetti songs. Alternative command: !co. ## TODO: -* [ ] Add more commands. -* [ ] Add tts messages to queue. -* [ ] Create log system. +* [X] Add more commands. +* [X] Add tts messages to queue. +* [X] Create log system. * [ ] Add more logs. * [ ] Add language support. * [ ] Create lang file. diff --git a/guild_queue.py b/guild_queue.py index ff0015f..d6f06eb 100644 --- a/guild_queue.py +++ b/guild_queue.py @@ -6,7 +6,8 @@ from discord.channel import VocalGuildChannel from discord.ext import tasks -from utils import remove_files +from tts import get_tts_dir_for_guild +from utils import remove_folder from voice import Sound, stop_and_disconnect, SoundType, play_sound @@ -18,7 +19,6 @@ def __init__(self, guild_id: int, vocal_channel: VocalGuildChannel, messageable: self.__sound_queue = sound_queue self.__voice_client = None self.__finished = False - self.__pending_files_for_deletion = [] def get_guild_id(self) -> int: return self.__guild_id @@ -52,12 +52,6 @@ def has_finished(self) -> bool: def set_finished(self, ready_for_removal: bool): self.__finished = ready_for_removal - def add_pending_file_for_deletion(self, path: str): - self.__pending_files_for_deletion.append(path) - - def get_pending_files_for_deletion(self) -> list[str]: - return self.__pending_files_for_deletion - guild_queues: list[GuildQueue] = [] @@ -80,7 +74,7 @@ async def cleanup_guild_queues(): not_finished_guild_queues.append(guild_queue) else: await stop_and_disconnect(guild_queue.get_voice_client()) - remove_files(guild_queue.get_pending_files_for_deletion()) + remove_folder(get_tts_dir_for_guild(guild_queue.get_guild_id())) guild_queues.clear() guild_queues.extend(not_finished_guild_queues) @@ -102,7 +96,7 @@ async def add_to_queue(guild_id: int, vocal_channel: VocalGuildChannel, messagea if guild_queue is not None: guild_queue.add_sound_to_queue(sound) if should_start_vitals and not queue_vitals.is_running(): - queue_vitals.start() # TODO doesn't work when method is run from another thread + queue_vitals.start() except Exception as e: log.error("add_to_queue >> Exception thrown when adding sound to queue.", exc_info=e) @@ -122,7 +116,6 @@ async def play_sounds_in_all_queues(): if sound.get_sound_type() is SoundType.TTS: await guild_queue.get_messageable().send(f":microphone: Reproduciendo un mensaje tts en `{voice_client.channel.name}`.") - guild_queue.add_pending_file_for_deletion(sound.get_source()) elif sound.get_sound_type() is not SoundType.FILE_SILENT: await guild_queue.get_messageable().send(f":notes: Reproduciendo `{sound.get_name()}` en `{voice_client.channel.name}`.") @@ -133,9 +126,7 @@ async def play_sounds_in_all_queues(): log.error("play_sounds_in_all_queues >> Caught exception playing sound. Removing sound queue for this guild...", exc_info=e) sound_queue = guild_queue.get_sound_queue() if len(sound_queue) > 0: - removed_sound = guild_queue.get_sound_queue().pop(0) - if removed_sound.get_sound_type() is SoundType.TTS: - guild_queue.add_pending_file_for_deletion(removed_sound.get_source()) + guild_queue.get_sound_queue().pop(0) if len(sound_queue) == 0: guild_queue.set_finished(True) diff --git a/main.py b/main.py index ebebb65..306bde1 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -import logging as log +import asyncio import random from datetime import datetime, time from threading import Event @@ -16,7 +16,7 @@ from guild_queue import add_to_queue, get_guild_queue from processors import process_link, process_reactions from threads import launch -from tts import generate_tts, tts_base_url +from tts import generate_tts from utils import * from voice import * from youtube import * @@ -300,9 +300,8 @@ def dalle_listener(result: DalleImages): def tts_listener(guild_id: int, vocal_channel: VocalGuildChannel, messageable: Messageable, original_file: str): - filename = original_file.replace(tts_base_url, "").replace(".mp3", "") - sound = Sound(filename, SoundType.TTS, original_file) - add_to_queue(guild_id, vocal_channel, messageable, sound) # TODO await + sound = Sound("mensaje tts", SoundType.TTS, original_file) + asyncio.run_coroutine_threadsafe(add_to_queue(guild_id, vocal_channel, messageable, sound), bot.loop) @tasks.loop(minutes=1) diff --git a/tts.py b/tts.py index 3019007..2f9dac8 100644 --- a/tts.py +++ b/tts.py @@ -12,48 +12,50 @@ from discord.channel import VocalGuildChannel from gtts import gTTS -tts_base_url = "./tts/" +from utils import remove_file, check_dir +TTS_DIR_BASE_PATH = "./tts" -def check_base_dir(): - if not os.path.exists(tts_base_url): - os.makedirs(tts_base_url) +def get_tts_dir_for_guild(guild_id: int) -> str: + return os.path.normpath(f"{TTS_DIR_BASE_PATH}/{guild_id}") -def get_loquendo_tts(text: str) -> Optional[str]: + +def get_loquendo_tts(text: str, dir_path: str) -> Optional[str]: try: url_encoded_text = quote(text) - file_name = f"{tts_base_url}tts_{str(time.time())}.mp3" + file_path = os.path.normpath(f"{dir_path}/tts_{str(time.time())}.mp3") text_to_hash = f"262mp3{text}" hash_digest = hashlib.md5(text_to_hash.encode()).hexdigest() url = f"https://cache-a.oddcast.com/c_fs/{hash_digest}.mp3?engine=2&language=2&voice=6&text={url_encoded_text}&useUTF8=1" - urlretrieve(url, file_name) - return file_name + urlretrieve(url, file_path) + return file_path except Exception: log.warning("TTS >> There's an error with the Loquendo TTS, trying with Google tts") - return get_google_tts(text) + return get_google_tts(text, dir_path) -def get_google_tts(text: str) -> str: +def get_google_tts(text: str, dir_path: str) -> str: tts = gTTS(text=text, lang='es', tld='es') - file_name = f"{tts_base_url}tts_{str(time.time())}.mp3" - check_base_dir() - tts.save(file_name) + file_path = os.path.normpath(f"{dir_path}/tts_{str(time.time())}.mp3") + tts.save(file_path) speed = get_speed(text) - file = change_speed(file_name, speed) - return file + file_path = change_speed(file_path, dir_path, speed) + return file_path def generate_tts(text: str, guild_id: int, vocal_channel: VocalGuildChannel, messageable: Messageable, listener: Callable[[int, VocalGuildChannel, Messageable, str], Any]): + tts_dir_path = get_tts_dir_for_guild(guild_id) + check_dir(tts_dir_path) if len(text) > 600: - audio = get_google_tts(text) + audio = get_google_tts(text, tts_dir_path) else: - audio = get_loquendo_tts(text) + audio = get_loquendo_tts(text, tts_dir_path) listener(guild_id, vocal_channel, messageable, audio) @@ -71,14 +73,14 @@ def get_speed(text: str) -> float: return 1.45 -def change_speed(file_name: str, speed: float) -> str: +def change_speed(file_path: str, dir_path: str, speed: float) -> str: try: - new_file_name = f"{tts_base_url}tts_{str(time.time())}.mp3" - ff = ffmpy.FFmpeg(inputs={file_name: None}, outputs={new_file_name: f"-filter:a atempo={speed}"}) + new_file_path = os.path.normpath(f"{dir_path}/tts_{str(time.time())}.mp3") + ff = ffmpy.FFmpeg(inputs={file_path: None}, outputs={new_file_path: f"-filter:a atempo={speed}"}) ff.run() - # TODO try this remove_file(file_name) - return new_file_name + remove_file(file_path) + return new_file_path - except Exception: - log.warning("TTS >> There's an error with the speed, trying with the original file") - return file_name + except Exception as e: + log.warning("TTS >> There's an error with the speed, trying with the original file", exc_info=e) + return file_path diff --git a/utils.py b/utils.py index 0e37029..ad22367 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,6 @@ import json import os +import shutil import traceback AUDIO_FOLDER_PATH = "./audio" @@ -79,6 +80,11 @@ def generate_sound_list_format(sounds: list[str]) -> list[list[str]]: return list_sounds +def check_dir(dir_path: str): + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + def remove_file(path: str): try: if os.path.exists(path): @@ -92,5 +98,13 @@ def remove_files(paths: list[str]): remove_file(path) +def remove_folder(dir_path: str): + try: + if os.path.exists(dir_path): + shutil.rmtree(dir_path) + except Exception: + traceback.print_exc() + + if __name__ == "__main__": pass From 796ed90618271f5353b7e63b871e68e16d98647a Mon Sep 17 00:00:00 2001 From: Javi <45560967+javdc@users.noreply.github.com> Date: Wed, 13 Mar 2024 21:22:43 +0100 Subject: [PATCH 3/3] Clear guild queue in disconnect command --- guild_queue.py | 2 +- main.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/guild_queue.py b/guild_queue.py index d6f06eb..01f4d5e 100644 --- a/guild_queue.py +++ b/guild_queue.py @@ -35,7 +35,7 @@ def get_sound_queue(self) -> list[Sound]: def add_sound_to_queue(self, sound: Sound): self.__sound_queue.append(sound) - def clear_queue(self): + def clear_sound_queue(self): self.__sound_queue.clear() def get_voice_client(self) -> Optional[VoiceClient]: diff --git a/main.py b/main.py index 306bde1..3b888ef 100644 --- a/main.py +++ b/main.py @@ -212,6 +212,9 @@ async def disconnect(ctx: Context): voice_client = guild.voice_client if isinstance(voice_client, VoiceClient): database.register_user_interaction(ctx.author.name, "disconnect") + guild_queue = get_guild_queue(guild.id) + if guild_queue is not None: + guild_queue.clear_sound_queue() await stop_and_disconnect(voice_client) await ctx.send(":robot: Desconectando...") else: