Skip to content

๐Ÿงฌ Samples

Teajay edited this page Oct 29, 2021 · 3 revisions

The following samples are for version 5.1.X. For updated and complete samples, please visit the examples branch.

โ–ถ๏ธ Auto Play

Auto-Play can be easily achieved using OnTrackEnded event and managing music requests using DefaultQueue.

private async Task OnTrackEnded(TrackEndedEventArgs args) {
    if (!args.Reason.ShouldPlayNext()) {
        return;
    }

    var player = args.Player;
    if (!player.Queue.TryDequeue(out var queueable)) {
        await player.TextChannel.SendMessageAsync("Queue completed! Please add more tracks to rock n' roll!");
        return;
    }

    if (!(queueable is LavaTrack track)) {
        await player.TextChannel.SendMessageAsync("Next item in queue is not a track.");
        return;
    }

    await args.Player.PlayAsync(track);
    await args.Player.TextChannel.SendMessageAsync(
        $"{args.Reason}: {args.Track.Title}\nNow playing: {track.Title}");
}
[Command("Play")]
public async Task PlayAsync([Remainder] string searchQuery) {
    if (string.IsNullOrWhiteSpace(searchQuery)) {
        await ReplyAsync("Please provide search terms.");
        return;
    }

    if (!_lavaNode.HasPlayer(Context.Guild)) {
        await ReplyAsync("I'm not connected to a voice channel.");
        return;
    }

    var queries = searchQuery.Split(' ');
    foreach (var query in queries) {
        var searchResponse = await _lavaNode.SearchAsync(query);
        if (searchResponse.LoadStatus == LoadStatus.LoadFailed ||
            searchResponse.LoadStatus == LoadStatus.NoMatches) {
            await ReplyAsync($"I wasn't able to find anything for `{query}`.");
            return;
        }

        var player = _lavaNode.GetPlayer(Context.Guild);

        if (player.PlayerState == PlayerState.Playing || player.PlayerState == PlayerState.Paused) {
            if (!string.IsNullOrWhiteSpace(searchResponse.Playlist.Name)) {
                foreach (var track in searchResponse.Tracks) {
                    player.Queue.Enqueue(track);
                }

                await ReplyAsync($"Enqueued {searchResponse.Tracks.Count} tracks.");
            }
            else {
                var track = searchResponse.Tracks[0];
                player.Queue.Enqueue(track);
                await ReplyAsync($"Enqueued: {track.Title}");
            }
        }
        else {
            var track = searchResponse.Tracks[0];

            if (!string.IsNullOrWhiteSpace(searchResponse.Playlist.Name)) {
                for (var i = 0; i < searchResponse.Tracks.Count; i++) {
                    if (i == 0) {
                        await player.PlayAsync(track);
                        await ReplyAsync($"Now Playing: {track.Title}");
                    }
                    else {
                        player.Queue.Enqueue(searchResponse.Tracks[i]);
                    }
                }

                await ReplyAsync($"Enqueued {searchResponse.Tracks.Count} tracks.");
            }
            else {
                await player.PlayAsync(track);
                await ReplyAsync($"Now Playing: {track.Title}");
            }
        }
    }
}

โš ๏ธ Exception Handling

All of the exceptions are logged to OnLog event. In order to catch exceptions and debug, you must subscribe to the event.

_lavaNode.OnLog += arg => {
    _logger.Log(arg.Severity.FromSeverityToLevel(), arg.Exception, arg.Message);
    return Task.CompletedTask;
};

๐Ÿ‘‰ Handling Track Stuck/Exception Events

Usually used to tackle Lavalink throwing track exceptions.

private async Task OnTrackException(TrackExceptionEventArgs arg) {
    _logger.LogError($"Track {arg.Track.Title} threw an exception. Please check Lavalink console/logs.");
    arg.Player.Queue.Enqueue(arg.Track);
    await arg.Player.TextChannel?.SendMessageAsync(
        $"{arg.Track.Title} has been re-added to queue after throwing an exception.");
}

private async Task OnTrackStuck(TrackStuckEventArgs arg) {
    _logger.LogError(
        $"Track {arg.Track.Title} got stuck for {arg.Threshold}ms. Please check Lavalink console/logs.");
    arg.Player.Queue.Enqueue(arg.Track);
    await arg.Player.TextChannel?.SendMessageAsync(
        $"{arg.Track.Title} has been re-added to queue after getting stuck.");
}

๐Ÿ‘‰ Try/Catch

In v5, throwing exceptions became a common practice instead of using TryXyz format where a bool would be returned. it is essential that you wrap methods in try/catch. A typical example would be:

[Command("Join")]
public async Task JoinAsync() {
    if (_lavaNode.HasPlayer(Context.Guild)) {
        await ReplyAsync("I'm already connected to a voice channel!");
        return;
    }

    var voiceState = Context.User as IVoiceState;
    if (voiceState?.VoiceChannel == null) {
        await ReplyAsync("You must be connected to a voice channel!");
        return;
    }

    try {
        await _lavaNode.JoinAsync(voiceState.VoiceChannel, Context.Channel as ITextChannel);
        await ReplyAsync($"Joined {voiceState.VoiceChannel.Name}!");
    }
    catch (Exception exception) {
        await ReplyAsync(exception.Message);
    }
}

๐Ÿ”Œ Auto Disconnect

With the addition of OnTrackStarted, you can easily manage auto-disconnect once your queue is finished. This example will utilize CancellationTokenSource and Tasks to handle our disconnect logic.

By no means this example is fail-proof or ideal. You're welcome to write your own disconnect logic.

๐Ÿ‘‰ Laying the foundation

In your AudioService, declare a new ConcurrentDictionary field with ulong as key and CancellationTokenSource as value. The key will be the voice channel ID.

private readonly ConcurrentDictionary<ulong, CancellationTokenSource> _disconnectTokens;

public AudioService(LavaNode lavaNode, ILoggerFactory loggerFactory) {
    _disconnectTokens = new ConcurrentDictionary<ulong, CancellationTokenSource>();
}

๐Ÿ‘‰ Building upon our foundation

We will then hook into OnTrackStarted event to handle auto-disconnect cancellation. We want to be able to cancel any disconnect tasks if a new track is added/started.

private async Task OnTrackStarted(TrackStartEventArgs arg) {
    if (!_disconnectTokens.TryGetValue(arg.Player.VoiceChannel.Id, out var value)) {
        return;
    }

    if (value.IsCancellationRequested) {
        return;
    }

    value.Cancel(true);
    await arg.Player.TextChannel.SendMessageAsync("Auto disconnect has been cancelled!");
}

๐Ÿ‘‰ Implementing disconnect logic

private async Task InitiateDisconnectAsync(LavaPlayer player, TimeSpan timeSpan) {
    if (!_disconnectTokens.TryGetValue(player.VoiceChannel.Id, out var value)) {
        value = new CancellationTokenSource();
        _disconnectTokens.TryAdd(player.VoiceChannel.Id, value);
    }
    else if (value.IsCancellationRequested) {
        _disconnectTokens.TryUpdate(player.VoiceChannel.Id, new CancellationTokenSource(), value);
        value = _disconnectTokens[player.VoiceChannel.Id];
    }

    await player.TextChannel.SendMessageAsync($"Auto disconnect initiated! Disconnecting in {timeSpan}...");
    var isCancelled = SpinWait.SpinUntil(() => value.IsCancellationRequested, timeSpan);
    if (isCancelled) {
        return;
    }

    await _lavaNode.LeaveAsync(player.VoiceChannel);
    await player.TextChannel.SendMessageAsync("Invite me again sometime, sugar.");
}

In our OnTrackEnded event, we want to start auto-disconnect task once our queue is empty.

private async Task OnTrackEnded(TrackEndedEventArgs args) {
    if (!args.Reason.ShouldPlayNext()) {
        return;
    }

    var player = args.Player;
    if (!player.Queue.TryDequeue(out var queueable)) {
        await player.TextChannel.SendMessageAsync("Queue completed! Please add more tracks to rock n' roll!");
        _ = InitiateDisconnectAsync(args.Player, TimeSpan.FromSeconds(10));
        return;
    }

    if (!(queueable is LavaTrack track)) {
        await player.TextChannel.SendMessageAsync("Next item in queue is not a track.");
        return;
    }

    await args.Player.PlayAsync(track);
    await args.Player.TextChannel.SendMessageAsync(
        $"{args.Reason}: {args.Track.Title}\nNow playing: {track.Title}");
}

๐ŸŽถ Fetching Lyrics

You want to spice up your bot? Say no more fam! Victoria offers 2 built-in lyric providers: OVH and Genius! Lyrics can be searched using the LyricsResolver class. Extension methods are also provided for LavaTrack: FetchLyricsFromGeniusAsync & FetchLyricsFromOVHAsync.

private static readonly IEnumerable<int> Range = Enumerable.Range(1900, 2000);

[Command("Genius", RunMode = RunMode.Async)]
public async Task ShowGeniusLyrics() {
    if (!_lavaNode.TryGetPlayer(Context.Guild, out var player)) {
        await ReplyAsync("I'm not connected to a voice channel.");
        return;
    }

    if (player.PlayerState != PlayerState.Playing) {
        await ReplyAsync("Woaaah there, I'm not playing any tracks.");
        return;
    }

    var lyrics = await player.Track.FetchLyricsFromGeniusAsync();
    if (string.IsNullOrWhiteSpace(lyrics)) {
        await ReplyAsync($"No lyrics found for {player.Track.Title}");
        return;
    }

    var splitLyrics = lyrics.Split('\n');
    var stringBuilder = new StringBuilder();
    foreach (var line in splitLyrics) {
        if (Range.Contains(stringBuilder.Length)) {
            await ReplyAsync($"```{stringBuilder}```");
            stringBuilder.Clear();
        }
        else {
            stringBuilder.AppendLine(line);
        }
    }

    await ReplyAsync($"```{stringBuilder}```");
}