How to implement music queue system for Discord bot

I am working on a Discord bot that can search and play music from YouTube. The basic functionality works fine - I can search for songs and play them in voice channels. However, I need help setting up a proper queue system that automatically plays the next song when the current one finishes.

Here’s my current implementation:

const CLIENT_KEY = "<your-api-key>";

var discordjs = require("discord.js");
var ytdlcore = require('ytdl-core');
var httprequest = require('superagent');

var client = new discordjs.Client();
var activeVoiceChannel = null;
var musicQueue = [];

client.on('ready', function() {
    console.log('Bot is online');
});

client.on('message', function(msg) {
    var cmdParts = msg.content.split(' ');
    var cmd = cmdParts[0].toLowerCase();
    var args = cmdParts.splice(1, cmdParts.length);

    switch (cmd) {
        case "hello":
            msg.reply("Hi there!");
            break;
        case "!commands":
            ShowHelp(msg);
            break;
        case "!connect":
            ConnectToVoice(args[0], msg);
            break;
        case "!music":
            SearchAndPlay(args.join(" "), msg);
            break;
    }
});

function ShowHelp(message) {
    message.reply("!connect <voice-channel> - Bot joins specified voice channel");
    message.reply("!music <search-query> - Searches and queues music from YouTube");
}

function SearchAndPlay(query) {
    FindYouTubeVideo(query);
}

function ConnectToVoice(channelName) {
    if (activeVoiceChannel) {
        activeVoiceChannel.disconnect();
    }
    var targetChannel = FindVoiceChannel(channelName);
    return targetChannel.join();
}

function FindVoiceChannel(channelName) {
    return client.channels.find(ch => ch.name === channelName);
}

function FindYouTubeVideo(searchQuery) {
    var apiUrl = 'https://www.googleapis.com/youtube/v3/search' + `?part=snippet&q=${escape(searchQuery)}&key=${CLIENT_KEY}`;
    
    httprequest(apiUrl, (err, resp) => {
        if (!err && resp.statusCode == 200) {
            var data = resp.body;
            
            for (var result of data.items) {
                if (result.id.kind === 'youtube#video') {
                    AddToQueue(result.id.videoId);
                }
            }
        }
    });
}

function AddToQueue(videoId) {
    var videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
    musicQueue.push(videoUrl);
}

function StartPlayback(url) {
    const playOptions = {seek: 0, volume: 0.8};
    
    if (url) {
        const audioStream = ytdlcore(url, {filter: 'audioonly'});
        const playDispatcher = client.voiceConnections.first().playStream(audioStream, playOptions);
    }
}

client.login("<bot-token>");

The main problem is detecting when a song ends so I can automatically start the next one in the queue. I tried using the ‘speaking’ event but it doesn’t work properly since the voice channel isn’t set up when the bot starts. Any suggestions on how to properly handle this queue system?

Your code’s got a solid foundation, but you’re missing how the queue and playback talk to each other. You need to hook StartPlayback into a proper queue manager. I’d build a processQueue function that handles starting playback and managing queue state. When StartPlayback runs, grab that dispatcher and slap some event listeners on it. Use the ‘end’ event instead of ‘finish’ - it’s way more reliable for catching when audio stops. Make sure you check if there are more songs queued before trying to play the next one. Handle the weird stuff too - manual stops, bot disconnects, that kind of thing. Store your voice connection globally so the queue system can actually reach it.

Yeah, this is super common with music bots. I hit the same issue six months ago on a similar project. You need a queue system that waits for the current song to finish before starting the next one. Store your dispatcher reference in StartPlayback and attach ‘end’ and ‘error’ listeners to it. When either fires, check if there’s more songs queued and call a recursive function for the next track. Don’t forget to shift/pop the finished song from your array. One gotcha - voice connection failures will crash your bot if you don’t check the connection exists before playing the next song. Learned that the hard way when someone kicked my bot mid-song.

Listen for the ‘finish’ event on the dispatcher. When you call playStream, it returns a dispatcher object - just do dispatcher.on(‘finish’, () => { playNextSong(); }) and it’ll trigger when the song ends. Also make sure your queue logic actually removes the played songs.