How to implement audio playlist functionality in Discord Bot

I’m working on creating a Discord bot that can handle music playback. Right now my bot can search YouTube and play single tracks, but I’m struggling with implementing a proper queue system.

The main problem is that I can’t figure out how to automatically start the next song when the current one finishes. I tried using the speaking event but it doesn’t work properly since the voice channel connection changes.

const YOUTUBE_API = "<api_key>";

var Discord = require("discord.js");
var ytDownload = require('ytdl-core');
var httpRequest = require('superagent');

var client = new Discord.Client();
var audioConnection = null;
var musicPlaylist = [];

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

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

    switch (mainCommand) {
        case "hello":
            msg.reply("Hi there!");
            break;
        case "!connect":
            ConnectToChannel(args[0], msg);
            break;
        case "!music":
            AddToPlaylist(args.join(" "), msg);
            break;
    }
});

function ConnectToChannel(channelName) {
    if (audioConnection) {
        audioConnection.disconnect();
    }
    
    var targetChannel = FindChannelByName(channelName);
    return targetChannel.join();
}

function AddToPlaylist(searchQuery) {
    SearchYouTube(searchQuery);
}

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

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

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

function StartPlayback(url) {
    const playOptions = {seek: 0, volume: 0.8};
    
    if (url) {
        const audioStream = ytDownload(url, {filter: 'audioonly'});
        const player = client.voiceConnections.first().playStream(audioStream, playOptions);
        
        player.on('end', () => {
            musicPlaylist.shift();
            if (musicPlaylist.length > 0) {
                StartPlayback(musicPlaylist[0]);
            }
        });
    }
}

client.login("<token_here>");

What’s the best way to detect when a song ends and automatically play the next one in the queue? Should I be using a different event or approach?

The problem is how you’re grabbing the voice connection. You’re calling client.voiceConnections.first() in StartPlayback every time, but that returns undefined if the connection state changes between songs. Store the connection when you first join and reuse it for the whole session. Your ‘end’ event on the dispatcher works, but you’re missing error handling. Add an ‘error’ listener that also advances the queue - otherwise a failed track kills everything. I hit this same issue with my bot. Tracks would randomly stop because YouTube access problems or connection drops threw errors instead of clean ends.

you’re using an old discord.js version - playStream is deprecated. switch to @discordjs/voice instead. it’s got proper AudioPlayer events like ‘stateChange’ that actually fire when tracks finish, way more stable than those old speaking events.

Your ‘end’ event isn’t firing consistently because dispatchers get flaky with network hiccups or when people skip tracks. Hit this same issue building my music bot last year. Here’s what fixed it: run a timeout alongside the end event. When a song starts, set a timer based on track duration (ytdl-core’s getInfo gives you this). Clear the timer if the end event fires normally. If the timeout hits first, something broke - just force the next song. Also, store your dispatcher reference globally so you can clean up properly between tracks. Keep the voice connection alive and just swap dispatchers for each new song.

This topic was automatically closed 4 days after the last reply. New replies are no longer allowed.