-
Notifications
You must be signed in to change notification settings - Fork 47
๐งฌ Samples
The following samples are for version 5.1.X
. For updated and complete samples, please visit the examples branch.
-
v5.2+
: https://github.com/Yucked/Victoria/tree/examples/v5 -
v6.0+
: https://github.com/Yucked/Victoria/tree/examples/v6
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}");
}
}
}
}
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;
};
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.");
}
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);
}
}
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.
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>();
}
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!");
}
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}");
}
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}```");
}