← All projects
Personal project

Discord Music Bot

A feature-rich Discord bot for music playback, queue management, and server monitoring with a web dashboard, soundboard, lyrics lookup, and persistent state across restarts.

Last updated October 2026

Overview

I wanted a music bot I could use with friends. I built an async Python bot with per-server queues and a separate web dashboard; SQLite tracks queue state, history, and favorites across restarts. It combines voice playback and server controls with soundboard clips and lyrics lookup, and restores active sessions after a restart.

What it is

Members can ask the bot to find a track by name or link, then control playback in a voice channel with skip, pause, volume, loop, and seek. It also supports bass boost, audio normalization, and instrumental playback.

Search, history, and playlist browsing use interactive Discord controls. A separate web dashboard provides remote playback and queue controls, favorites, and system monitoring.

Why I built it

Discord bots are a natural fit for practicing asynchronous Python—everything is event-driven through WebSocket gateway connections, and voice channels add real-time streaming on top of that. I wanted hands-on experience with async/await patterns, concurrency, and long-running services beyond typical request-response web apps.

I also wanted a project I could actually use with friends. Building something people depend on for shared listening sessions raised the stakes beyond throwaway side projects and forced me to handle edge cases—disconnects, silent failures, and state recovery—that tutorials never cover.

Architecture

The bot is built on discord.py with command prefixes and is structured into cogs for modularity. The Music cog manages per-guild VoiceState objects, each owning their own queue, audio player loop, and Discord voice connection. A HistoryService layer abstracts persistence through SQLite, tracking plays, searches, favorites, and queue state so everything survives restarts.

A separate aiohttp-based web server runs alongside the bot, serving a REST API with 25+ endpoints and a static frontend. Audio resolution uses yt-dlp for YouTube and SoundCloud with FFmpeg transcoding. The now-playing canvas is rendered server-side using Pillow, and the whole stack is containerized with Docker and deployed through docker-compose with persistent volumes.

Diagram showing Discord UI and web dashboard flowing through a discord.py bot and aiohttp API to a yt-dlp and FFmpeg audio pipeline, with SQLite storing queues, history, and sessions.
Take a closer look
Discord commands and the web dashboard control per-server queues; yt-dlp and FFmpeg supply audio for Discord voice playback.

What I worked through

Voice connections in Discord are unreliable in ways the documentation does not fully capture. The bot detects silent disconnects, reconnects gracefully without duplicating state, and restores active sessions after restart by persisting voice channel IDs and current song position to the database.

Audio streaming required careful resource management—each track goes through yt-dlp resolution and FFmpeg transcoding, and the player must clean up subprocesses, temporary files, and audio buffers between every song. Edge cases like empty queues, unavailable videos, permission changes mid-playback, rate limits, and the player loop restarting itself after failures were all production bugs that had to be handled without crashing the entire bot.

Lessons learned

This project taught me that real-time systems are unforgiving in ways tutorials never show. Discord's voice API has failure modes the documentation barely mentions—silent disconnects, stale connections that look active, and rate limits that punish naive retry loops. Building something people actually used forced me to handle every one of those edge cases instead of hoping they would not happen.

The deeper lesson was about long-running service design. Unlike a request-response app that starts and stops cleanly, a bot runs indefinitely and accumulates state. Being forced to think about session persistence, graceful restarts, and resource cleanup was the single biggest architectural lesson of the project.

Highlights

  • Built an event-driven Discord bot handling real-time voice connections, audio streaming, and WebSocket gateway event processing using async Python
  • Designed a modular architecture with per-guild voice state management, persistent queue persistence, and automatic session restoration across service restarts
  • Integrated track resolution from YouTube and SoundCloud through yt-dlp with FFmpeg-based audio transcoding and gapless playback
  • Built a web dashboard with 25+ REST API endpoints and a static frontend for remote play control, queue management, favorites, and system monitoring
  • Implemented interactive Discord UI components including a paginated search view, history browser, and a server-side rendered now-playing canvas using Pillow
  • Containerized the entire bot with Docker and deployed as a long-lived service alongside 15+ existing homelab services

Code

Server-Side Now-Playing Canvas with Dynamic Color Extraction

python
class CanvasGenerator:
    """Renders a real-time now-playing canvas with dynamic accent color
    extracted from the album art — no hardcoded theme."""

    async def _generate_all_layers(self, song):
        width, height = 1920, 500

        # --- Extract dominant accent color from thumbnail ---
        async with self.session.get(song.thumbnail) as resp:
            img = Image.open(io.BytesIO(await resp.read())).convert('RGBA')

            # Resize to 50×50 and find most common vibrant pixel
            small = img.resize((50, 50))
            pixels = [small.getpixel((x, y))
                      for x in range(50) for y in range(50)]
            vibrant = [p for p in pixels
                       if 100 < sum(p[:3]) < 650]
            accent = collections.Counter(vibrant).most_common(1)[0][0][:3]

        # --- Blurred thumbnail as background ---
        scale = max(width / img.width, height / img.height) * 1.5
        bg_thumb = img.resize(
            (int(img.width * scale), int(img.height * scale)),
            Image.Resampling.LANCZOS)
        cx, cy = (bg_thumb.width - width) / 2, (bg_thumb.height - height) / 2
        bg = bg_thumb.crop((cx, cy, cx + width, cy + height))
        bg = bg.filter(ImageFilter.GaussianBlur(30))

        # --- Thumbnail with rounded corners + drop shadow ---
        thumb = img.resize((380, 380), Image.Resampling.LANCZOS)
        mask = Image.new('L', (380, 380), 0)
        ImageDraw.Draw(mask).rounded_rectangle(
            [0, 0, 380, 380], radius=40, fill=255)

        # --- Progress bar with accent color ---
        bar_width, bar_x, bar_y = width - 560, 500, 330
        progress = max(0, min(elapsed / song.duration_int, 1.0))
        p_w = int(bar_width * progress)

        draw = ImageDraw.Draw(canvas)
        draw.rounded_rectangle(
            [bar_x, bar_y, bar_x + bar_width, bar_y + 10],
            radius=5, fill=(60, 60, 70))
        if p_w > 0:
            draw.rounded_rectangle(
                [bar_x, bar_y, bar_x + p_w, bar_y + 10],
                radius=5, fill=accent)
            draw.ellipse([bar_x + p_w - 8, bar_y - 3,
                          bar_x + p_w + 8, bar_y + 13],
                         fill=accent)

        # --- UP NEXT with cached thumbnail ---
        nx, ny = 500, 395
        canvas.paste(next_thumb, (nx, ny), next_mask)
        draw.text((nx + 115, ny + 5), "UP NEXT",
                  font=get_font(20, bold=True),
                  fill=(255, 215, 100, 180))
        draw.text((nx + 115, ny + 35),
                  next_song.title[:60],
                  font=get_font(30),
                  fill=(255, 255, 255, 220))

Every time a song plays, this renders a 1920×500 canvas server-side using Pillow: the album art is sampled at 50×50 to extract the dominant accent color, then blurred at 30px Gaussian to become the background. The thumbnail gets rounded corners with a matching accent border, requester avatar is fetched and circle-cropped, and status badges (BASS BOOST, LOOPING, NORMALIZED) are drawn dynamically. The “UP NEXT” thumbnail is cached at 90px to avoid re-fetching. Canvas layers are cached per (song_url, requester_id) with an LRU eviction policy.

Skills & Technologies

Pythondiscord.pyAsync Pythonyt-dlpFFmpegaiohttpSQLiteDockerREST APIsHTMLCSSJavaScriptWeb Dashboard

Glossary

aiohttp
A Python library for making and serving web requests without blocking other tasks.
Async Python
A way to let Python handle other tasks while waiting for network requests or other slow operations.
discord.py
A Python library for building bots that interact with Discord.
FFmpeg
A toolkit for processing and converting audio and video.
REST APIs
Web interfaces that let software exchange data through standard requests, such as reading or updating a resource.
yt-dlp
A tool for retrieving audio and video from supported websites.
See more glossary terms