Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Melendo/BotMeriendo/llms.txt

Use this file to discover all available pages before exploring further.

BotMeriendo is structured around discord.py’s Cog system — each functional area (music, general, events) is a separate Python module loaded dynamically at startup. This separation keeps concerns isolated: music playback logic never touches event handling, and shared state is managed in one place. The entry point (src/main.py) wires everything together, while src/utils/ provides logging and queue state that every cog imports.

Entry Point

src/main.py is the single file you run to start the bot. On launch it performs these steps in order:
1

Load and validate configuration

Imports TOKEN, PREFIX, and validate_config from src/config.py. validate_config() raises a ValueError immediately if TOKEN is missing, preventing a silent startup failure.
2

Create the bot instance

Builds a commands.Bot with discord.Intents.all() and explicitly sets intents.message_content = True so prefix commands can read message bodies.
intents = discord.Intents.all()
intents.message_content = True

bot = commands.Bot(command_prefix=PREFIX, intents=intents, description=descripcion)
3

Load extensions

load_extensions() iterates over a hardcoded list and calls bot.load_extension() for each cog. Any extension that fails to load is logged at ERROR level, but the bot continues starting up with the remaining extensions.
async def load_extensions():
    extensions = [
        "src.cogs.general",
        "src.cogs.music",
        "src.cogs.events"
    ]
    for ext in extensions:
        try:
            await bot.load_extension(ext)
            logger.info(f"Cargado extensión: {ext}")
        except Exception as e:
            logger.error(f"Error cargando extensión {ext}: {e}")
4

Register signal handlers and run

Before calling asyncio.run(main()), the __main__ block registers OS-level signal handlers for SIGTERM and SIGINT. When either signal arrives, all running asyncio tasks are cancelled and a shutdown message is logged before the process exits.
def shutdown():
    tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
    for task in tasks:
        task.cancel()
    logger.info("Cierre del bot iniciado...")

loop.add_signal_handler(signal.SIGTERM, shutdown)
loop.add_signal_handler(signal.SIGINT, shutdown)
This is what makes docker compose down (which sends SIGTERM) produce a clean shutdown log instead of a hard kill.

Module Map

src/cogs/general.py

Basic utility commands: !ping, !hola, and !comandos (a custom help embed that groups commands by category).

src/cogs/music.py

Full YouTube music streaming via yt_dlp, queue management, playlist support, and interactive UI buttons (pause/resume, skip) rendered as Discord message components.

src/cogs/events.py

Discord gateway event listeners: on_ready, on_member_join, on_command_error, and on_voice_state_update (auto-disconnect and queue cleanup).

src/utils/logger.py

Logging setup that writes to both bot.log (file) and stdout simultaneously. Configures log levels per-namespace to reduce discord.py noise.

src/utils/state.py

Defines the shared music_queues dict (keyed by guild ID). src/cogs/music.py imports it directly; src/cogs/events.py imports it via src.cogs.music. Single source of truth for per-guild queue state.

src/config.py

Loads TOKEN and TRGGKEY from the .env file via python-dotenv. Provides validate_config() and derives PREFIX (TRGGKEY value, or ! if unset).

Project Directory Tree

botMeriendo/
├── src/
│   ├── cogs/          # Módulos del bot
│   │   ├── events.py  # Manejo de eventos (join, errores, voz)
│   │   ├── general.py # Comandos básicos
│   │   └── music.py   # Lógica de música
│   ├── utils/         # Utilidades
│   │   ├── logger.py  # Configuración de logs
│   │   └── state.py   # Estado compartido (colas)
│   ├── config.py      # Configuración y validación
│   └── main.py        # Punto de entrada
├── Dockerfile         # Imagen de Docker
├── docker-compose.yml # Orquestación de contenedores
└── requirements.txt   # Dependencias Python

Shared State

Queue state lives entirely in src/utils/state.py:
# Diccionario para colas de música por servidor
music_queues = {}
music_queues is a plain Python dict keyed by guild ID (int). Each value is a list of (video_url, title) tuples where video_url is the canonical YouTube watch URL and title is the human-readable video name. src/cogs/music.py imports music_queues directly from src.utils.state. src/cogs/events.py imports it from src.cogs.music (which re-exports the same object), so mutations from either cog are immediately visible to the other. Because it is a plain in-memory dict:
  • No persistence — the queue is completely discarded when the bot process exits or restarts.
  • No concurrency control — Python’s GIL and asyncio’s single-threaded event loop make concurrent mutations safe within one process, but there is no cross-process or cross-shard safety.
  • Per-guild isolation — each guild gets its own list, so one server’s queue never interferes with another’s.
If the bot crashes or is restarted mid-playback, the queue for every guild is lost. Users will need to re-add their songs. For persistent queues, you would need to replace music_queues with a database-backed store.

Audio Pipeline

BotMeriendo uses a JIT (Just-In-Time) audio resolution pattern to avoid stale stream URLs when managing playlists or multi-song queues.
1

Queue fast metadata only

When !play is called, yt_dlp runs with extract_flat: 'in_playlist' for URL inputs. This retrieves only the video’s watch URL and title — it does not resolve the audio stream. The lightweight (video_url, title) tuple is appended to music_queues[guild_id] immediately.
music_queues[guild_id].append((video_url, video_title))
2

Resolve the stream URL just before playback

When play_next_song() pops the next item from the queue, it calls ytdl.extract_info() at that moment — not during enqueueing. This runs in a thread via asyncio.to_thread so it doesn’t block the event loop while yt-dlp fetches the audio manifest.
video_url, title = music_queues[guild_id].pop(0)

# RESOLUCION JIT: Extraemos el stream de audio aqui, justo antes de reproducir
# Usamos asyncio.to_thread para no bloquear el loop mientras yt-dlp procesa
info = await asyncio.to_thread(ytdl.extract_info, video_url, download=False)
audio_source_url = info['url']  # Esta es la URL real del stream de audio
3

Open the FFmpeg audio source

The resolved stream URL is passed to discord.FFmpegOpusAudio.from_probe() along with FFmpeg reconnect options. If the stream drops mid-play, FFmpeg automatically attempts to reconnect for up to 5 seconds.
source = await discord.FFmpegOpusAudio.from_probe(audio_source_url, **ffmpeg_options)
Where ffmpeg_options is:
ffmpeg_options = {
    'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
    'options': '-vn'
}
4

Chain to the next song via callback

The after callback passed to voice_client.play() calls play_next_song() again via asyncio.run_coroutine_threadsafe, creating a self-chaining playback loop that continues until the queue is empty.
The JIT pattern means a playlist of 50 songs can be enqueued in under a second, but each song incurs a small resolution delay (usually under 2 seconds) just before it starts playing. This is the intended trade-off: fast queue-add at the cost of a brief per-song startup pause.

Build docs developers (and LLMs) love