Skip to content

Basic Music Bot

A clean, minimal music bot covering the essential commands. Perfect as a starting point.

basic_bot.py
import os
import discord
from discord.ext import commands
from dotenv import load_dotenv
import voltricx

load_dotenv()

# ── Bot Setup ──────────────────────────────────────────────────────────────
class MusicBot(commands.Bot):
    def __init__(self):
        intents = discord.Intents.default()
        intents.message_content = True
        intents.voice_states = True
        super().__init__(command_prefix="!", intents=intents, help_command=None)

    async def setup_hook(self):
        await voltricx.Pool.connect(
            client=self,
            nodes=[
                voltricx.NodeConfig(
                    identifier="Main",
                    uri=os.getenv("LAVALINK_URI", "http://localhost:2333"),
                    password=os.getenv("LAVALINK_PASSWORD", "youshallnotpass"),
                )
            ],
            cache_config={"capacity": 100, "track_capacity": 500},
            default_search_source="dzsearch",
        )

    async def close(self):
        await voltricx.Pool.close()
        await super().close()


bot = MusicBot()


# ── Events ─────────────────────────────────────────────────────────────────
@bot.event
async def on_ready():
    print(f"🎵 {bot.user} is ready!")

@bot.event
async def on_voltricx_node_ready(node: voltricx.Node):
    print(f"✅ Node {node.identifier!r} connected")

@bot.event
async def on_voltricx_track_start(player: voltricx.Player, track: voltricx.Playable):
    if player.home:
        embed = discord.Embed(
            title="🎵 Now Playing",
            description=f"**{track.title}**\nby {track.author}",
            color=0x7c3aed,
        )
        if track.artwork:
            embed.set_thumbnail(url=track.artwork)
        embed.add_field(
            name="Duration",
            value=f"{track.length // 60000}:{(track.length // 1000) % 60:02d}" if not track.is_stream else "🔴 LIVE",
        )
        embed.add_field(name="Source", value=track.source.capitalize())
        await player.home.send(embed=embed)

@bot.event
async def on_voltricx_inactive_player(player: voltricx.Player):
    if player.home:
        await player.home.send("💤 Left the channel due to inactivity.")
    await player.disconnect()

@bot.event
async def on_command_error(ctx: commands.Context, error: Exception):
    error = getattr(error, "original", error)
    if isinstance(error, voltricx.VoltricxException):
        await ctx.send(f"❌ **Music Error**: {error}")
    elif isinstance(error, commands.MissingRequiredArgument):
        await ctx.send(f"❌ Missing argument: `{error.param.name}`")
    elif not isinstance(error, commands.CommandNotFound):
        raise error


# ── Helper ─────────────────────────────────────────────────────────────────
def get_player(ctx: commands.Context) -> voltricx.Player | None:
    return ctx.voice_client  # type: ignore


# ── Commands ───────────────────────────────────────────────────────────────
@bot.command()
async def join(ctx: commands.Context):
    """Join your voice channel."""
    if not ctx.author.voice:
        return await ctx.send("❌ You must be in a voice channel.")
    if ctx.voice_client:
        return await ctx.send("Already connected.")
    player: voltricx.Player = await ctx.author.voice.channel.connect(cls=voltricx.Player)
    player.home = ctx.channel
    player.inactive_timeout = 300  # 5 minutes
    await ctx.send(f"✅ Joined **{ctx.author.voice.channel.name}**")


@bot.command(aliases=["p"])
async def play(ctx: commands.Context, *, query: str):
    """Play a track or add it to the queue."""
    if not ctx.author.voice:
        return await ctx.send("❌ Join a voice channel first!")

    if not ctx.voice_client:
        player: voltricx.Player = await ctx.author.voice.channel.connect(cls=voltricx.Player)
        player.home = ctx.channel
        player.inactive_timeout = 300
    else:
        player: voltricx.Player = ctx.voice_client

    async with ctx.typing():
        results = await voltricx.Pool.fetch_tracks(query)

    if not results:
        return await ctx.send(f"❌ No results for `{query}`")

    if isinstance(results, voltricx.Playlist):
        count = player.queue.put(results)
        await ctx.send(f"📋 Added playlist **{results.name}** ({count} tracks)")
    else:
        track = results[0]
        player.queue.put(track)
        await ctx.send(f"➕ Added **{track.title}** to queue")

    if not player.playing:
        await player.play(player.queue.get())


@bot.command(aliases=["s"])
async def skip(ctx: commands.Context):
    """Skip the current track."""
    player = get_player(ctx)
    if not player:
        return await ctx.send("❌ Not in a voice channel.")
    old = player.current
    await player.skip()
    await ctx.send(f"⏭️ Skipped **{old.title if old else 'track'}**")


@bot.command()
async def pause(ctx: commands.Context):
    """Pause playback."""
    player = get_player(ctx)
    if player:
        await player.pause(True)
        await ctx.send("⏸️ Paused.")


@bot.command()
async def resume(ctx: commands.Context):
    """Resume playback."""
    player = get_player(ctx)
    if player:
        await player.pause(False)
        await ctx.send("▶️ Resumed.")


@bot.command(aliases=["vol"])
async def volume(ctx: commands.Context, level: int):
    """Set volume (0–1000)."""
    player = get_player(ctx)
    if player:
        await player.set_volume(level)
        await ctx.send(f"🔊 Volume: **{level}%**")


@bot.command(aliases=["np"])
async def nowplaying(ctx: commands.Context):
    """Show current track with progress bar."""
    player = get_player(ctx)
    if not player or not player.current:
        return await ctx.send("Nothing is playing.")

    track = player.current
    pos, length = player.position, track.length
    filled = int((pos / length) * 20) if length else 0
    bar = "▬" * filled + "🔘" + "▬" * max(0, 20 - filled)

    fmt = lambda ms: f"{ms // 60000}:{(ms // 1000) % 60:02d}"

    embed = discord.Embed(
        title="🎵 Now Playing",
        description=f"**[{track.title}]({track.uri})**\nby {track.author}",
        color=0x7c3aed,
    )
    embed.add_field(
        name="Progress",
        value=f"`{fmt(pos)}` {bar} `{fmt(length)}`",
        inline=False,
    )
    embed.add_field(name="Volume", value=f"{player.volume}%")
    embed.add_field(name="Queue", value=f"{len(player.queue)} tracks")
    if track.artwork:
        embed.set_thumbnail(url=track.artwork)
    await ctx.send(embed=embed)


@bot.command(aliases=["q"])
async def queue(ctx: commands.Context):
    """Show the queue."""
    player = get_player(ctx)
    if not player or not player.queue:
        return await ctx.send("The queue is empty.")

    tracks = player.queue[0:10]
    lines = [f"`{i+1}.` {t.title}" for i, t in enumerate(tracks)]
    embed = discord.Embed(
        title=f"📋 Queue — {len(player.queue)} tracks",
        description="\n".join(lines),
        color=0x7c3aed,
    )
    if player.current:
        embed.set_author(name=f"▶ {player.current.title}")
    if len(player.queue) > 10:
        embed.set_footer(text=f"Showing 10/{len(player.queue)} tracks")
    await ctx.send(embed=embed)


@bot.command()
async def seek(ctx: commands.Context, position: str):
    """Seek to m:ss or seconds."""
    player = get_player(ctx)
    if not player:
        return
    try:
        if ":" in position:
            m, s = map(int, position.split(":"))
            ms = (m * 60 + s) * 1000
        else:
            ms = int(position) * 1000
        await player.seek(ms)
        await ctx.send(f"⏩ Seeked to `{position}`")
    except ValueError:
        await ctx.send("❌ Use `m:ss` or `seconds`")


@bot.command()
async def stop(ctx: commands.Context):
    """Stop and clear the queue."""
    player = get_player(ctx)
    if player:
        player.queue.clear()
        await player.stop()
        await ctx.send("⏹️ Stopped and cleared queue.")


@bot.command()
async def disconnect(ctx: commands.Context):
    """Disconnect from voice."""
    player = get_player(ctx)
    if player:
        await player.disconnect()
        await ctx.send("👋 Disconnected.")


@bot.command()
async def shuffle(ctx: commands.Context):
    """Shuffle the queue."""
    player = get_player(ctx)
    if player and player.queue:
        player.queue.shuffle()
        await ctx.send("🔀 Queue shuffled!")


@bot.command()
async def loop(ctx: commands.Context, mode: str = "normal"):
    """Set loop mode: normal, loop, loop_all."""
    player = get_player(ctx)
    if not player:
        return
    try:
        player.queue.mode = voltricx.QueueMode(mode.lower())
        await ctx.send(f"🔁 Loop mode: **{mode}**")
    except ValueError:
        await ctx.send("❌ Use: `normal`, `loop`, or `loop_all`")


@bot.command()
async def help(ctx: commands.Context):
    """Show all commands."""
    embed = discord.Embed(title="🎵 Voltricx Music Bot", color=0x7c3aed)
    embed.add_field(name="Playback", value=(
        "`!play <query>` `!skip` `!pause` `!resume`\n"
        "`!stop` `!seek <time>` `!volume <0-1000>`"
    ), inline=False)
    embed.add_field(name="Queue", value=(
        "`!queue` `!shuffle` `!loop <mode>`\n"
        "`!nowplaying` `!disconnect`"
    ), inline=False)
    await ctx.send(embed=embed)


bot.run(os.getenv("TOKEN"))