Skip to content

Commit a71d777

Browse files
committed
Update retrying flow
1 parent d169f00 commit a71d777

6 files changed

Lines changed: 98 additions & 42 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
namespace Application.DTOs;
22

3-
public record PlayRequest<TContext>(TContext Context, Func<Task> Callbacks);
3+
public record PlayRequest<TContext>(TContext Context, Func<string, Task> Callbacks);

src/Application/Interfaces/Services/INetCordAudioPlayerService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ public interface INetCordAudioPlayerService
44
{
55
event Func<Task>? DisconnectedVoiceClientEvent;
66
event Func<Task>? NotInVoiceChannelCallback;
7-
Task Play<T>(T ctx, Action<Func<Task>> disconnectAsync);
7+
Task Play(Action<Func<Task>> disconnectAsync);
88
}

src/Infrastructure/Commands/NetCordCommand.cs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
using System.ComponentModel;
21
using Application.Interfaces.Services;
32
using Domain.Common;
4-
using Domain.Entities;
53
using Domain.Eventing;
6-
using Infrastructure.Services;
74
using Microsoft.Extensions.Configuration;
85
using Microsoft.Extensions.DependencyInjection;
96
using NetCord.Rest;
7+
using NetCord.Services;
108
using NetCord.Services.ApplicationCommands;
119
using NetCord.Services.Commands;
1210
using YoutubeExplode;
1311
using YoutubeExplode.Common;
14-
using YoutubeExplode.Search;
1512
using Constants = Domain.Common.Constants;
1613

1714
namespace Infrastructure.Commands;
@@ -20,15 +17,18 @@ public class NetCordCommand(IServiceProvider serviceProvider, IConfiguration con
2017
: ApplicationCommandModule<ApplicationCommandContext>
2118
{
2219
[SlashCommand("play", "Play a track from SoundCloud")]
23-
[Description("Either song title, youtube URL or 'radio'")]
2420
public async Task PingAsync([CommandParameter(Remainder = true)] string command)
2521
{
22+
if (await NotInVoiceChannel())
23+
{
24+
return;
25+
}
2626
// `AddApplicationCommands()` registers services as singleton,
2727
// so scope is needed to resolve scoped services.
2828
using var scope = serviceProvider.CreateScope();
2929
var youtubeClient = scope.ServiceProvider.GetRequiredService<YoutubeClient>();
3030
var radioSourceService = scope.ServiceProvider.GetRequiredService<IRadioSourceService>();
31-
31+
3232
var message = CreateMessage<InteractionMessageProperties>("Select a track to play:");
3333

3434
switch (command)
@@ -38,7 +38,8 @@ public async Task PingAsync([CommandParameter(Remainder = true)] string command)
3838
case "Radio":
3939
{
4040
var radiosSourceList = (await radioSourceService.GetAllRadioSourcesAsync()).Where(rs => rs.IsActive);
41-
message.Components = CreateComponent(radiosSourceList.Select(rs => new ComponentModel(rs.Name, rs.Id.ToString())));
41+
message.Components =
42+
CreateComponent(radiosSourceList.Select(rs => new ComponentModel(rs.Name, rs.Id.ToString())));
4243
break;
4344
}
4445
case var _ when Uri.TryCreate(command, UriKind.Absolute, out _):
@@ -52,17 +53,22 @@ public async Task PingAsync([CommandParameter(Remainder = true)] string command)
5253
await youtubeClient.Search.GetVideosAsync(command)
5354
.CollectAsync(5);
5455

55-
message.Components = CreateComponent(source.Select(s => new ComponentModel(s.Title, s.Url, s.Author.ChannelTitle)));
56+
message.Components =
57+
CreateComponent(source.Select(s => new ComponentModel(s.Title, s.Url, s.Author.ChannelTitle)));
5658
}
5759
break;
5860
}
59-
61+
6062
await RespondAsync(InteractionCallback.Message(message));
6163
}
6264

6365
[SlashCommand("stop", "Stop playing and clear the queue")]
6466
public async Task Stop()
6567
{
68+
if (await NotInVoiceChannel())
69+
{
70+
return;
71+
}
6672
using var scope = serviceProvider.CreateScope();
6773
var eventDispatcher = scope.ServiceProvider.GetRequiredService<IEventDispatcher>();
6874
eventDispatcher.Dispatch(new EventType.Stop());
@@ -73,6 +79,10 @@ public async Task Stop()
7379
[SlashCommand("skip", "Skip the current track")]
7480
public async Task Skip()
7581
{
82+
if (await NotInVoiceChannel())
83+
{
84+
return;
85+
}
7686
using var scope = serviceProvider.CreateScope();
7787
var eventDispatcher = scope.ServiceProvider.GetRequiredService<IEventDispatcher>();
7888
eventDispatcher.Dispatch(new EventType.Skip());
@@ -103,6 +113,19 @@ private static IEnumerable<IMessageComponentProperties> CreateComponent<T>(T sou
103113
}
104114
];
105115
}
116+
117+
private async Task<bool> NotInVoiceChannel()
118+
{
119+
if (!Context.Guild!.VoiceStates.TryGetValue(Context.User.Id, out _))
120+
{
121+
var notInVoiceChannelMessage =
122+
CreateMessage<InteractionMessageProperties>("You must be in a voice channel to use this command.");
123+
await RespondAsync(InteractionCallback.Message(notInVoiceChannelMessage));
124+
return true;
125+
}
126+
127+
return false;
128+
}
106129

107130
private record ComponentModel(string Title, string Url, string? Description = null);
108131
}

src/Infrastructure/Interaction/NetCordInteraction.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,21 @@ public class NetCordInteraction(
1919
[ComponentInteraction(Constants.CustomIds.Play)]
2020
public async Task<string> Play()
2121
{
22+
var context = Context;
23+
if (!NotInVoiceChannel())
24+
{
25+
return "You must be in a voice channel to use this command.";
26+
}
27+
28+
if (!NotDeafened())
29+
{
30+
return "You must be deafened to use this command.";
31+
}
32+
2233
logger.LogInformation("Play command invoked by user {UserId} in guild {GuildId}", Context.User.Id,
2334
Context.Guild?.Id);
2435

25-
var playRequest = new PlayRequest<StringMenuInteractionContext>(Context, NotInVoiceChannelCallback);
36+
var playRequest = new PlayRequest<StringMenuInteractionContext>(Context, RespondAsyncCallback);
2637
queueService.Enqueue(playRequest);
2738
eventDispatcher.Dispatch(new EventType.Play());
2839
var selectedValue = Context.SelectedValues[0];
@@ -40,6 +51,29 @@ public async Task<string> Play()
4051

4152
return message;
4253

43-
Task<InteractionCallbackResponse> NotInVoiceChannelCallback() => RespondAsync(InteractionCallback.Message("You are not connected to any voice channel!"))!;
4454
}
55+
56+
private bool NotInVoiceChannel()
57+
{
58+
return Context.Guild!.VoiceStates.TryGetValue(Context.User.Id, out _);
59+
}
60+
61+
private bool NotDeafened()
62+
{
63+
var voiceState = Context.Guild!.VoiceStates[Context.User.Id];
64+
return !(voiceState.IsDeafened || voiceState.IsSelfDeafened);
65+
}
66+
67+
private bool UserAndBotInSameVoiceChannel()
68+
{
69+
var voiceState = Context.Guild!.VoiceStates[Context.User.Id];
70+
if (!Context.Guild.VoiceStates.TryGetValue(Context.Client.Id, out var botVoiceState))
71+
{
72+
return false;
73+
}
74+
75+
return voiceState.ChannelId == botVoiceState.ChannelId;
76+
}
77+
78+
private Task<InteractionCallbackResponse> RespondAsyncCallback(string message) => RespondAsync(InteractionCallback.Message(message))!;
4579
}

src/Infrastructure/Services/NetCordAudioPlayerService.cs

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,34 @@ public class NetCordAudioPlayerService(
2020
public event Func<Task>? DisconnectedVoiceClientEvent;
2121
public event Func<Task>? NotInVoiceChannelCallback;
2222
private Action<Func<Task>> OnDisconnectAsync { get; set; } = _ => { };
23-
private StringMenuInteractionContext Context { get; set; } = null!;
2423

25-
private int _retryCount = 0;
24+
private int _retryCount;
25+
private bool _retrying;
2626

27-
public async Task Play<T>(T ctx, Action<Func<Task>> onDisconnectAsync)
27+
public async Task Play(Action<Func<Task>> onDisconnectAsync)
2828
{
29-
if (ctx is not StringMenuInteractionContext context)
30-
{
31-
throw new ArgumentException("Invalid context type. Expected StringMenuInteractionContext.", nameof(ctx));
32-
}
33-
3429
OnDisconnectAsync = onDisconnectAsync;
35-
Context = context;
36-
3730
await HandleMusicPlayingAsync();
3831
}
3932

4033
private async Task HandleMusicPlayingAsync()
4134
{
42-
var guild = Context.Guild!;
35+
var currentTrack = queue.Peek<StringMenuInteractionContext>();
36+
37+
if (currentTrack is null)
38+
{
39+
logger.LogError("Current track is null");
40+
return;
41+
}
42+
var guild = currentTrack.Context.Guild!;
4343
// Get the user voice state
44-
if (!guild.VoiceStates.TryGetValue(Context.User.Id, out var voiceState))
44+
if (!guild.VoiceStates.TryGetValue(currentTrack.Context.User.Id, out var voiceState))
4545
{
4646
await (NotInVoiceChannelCallback?.Invoke() ?? Task.CompletedTask);
4747
return;
4848
}
4949

50-
var client = Context.Client;
50+
var client = currentTrack.Context.Client;
5151

5252
if (playerState.CurrentAction == PlayerAction.Stop)
5353
{
@@ -74,13 +74,6 @@ async Task HandleVoiceStream()
7474
var radioSourceService = scope.ServiceProvider
7575
.GetRequiredService<IRadioSourceService>();
7676

77-
var currentTrack = queue.Peek<StringMenuInteractionContext>();
78-
if (currentTrack is null)
79-
{
80-
logger.LogError("Current track is null");
81-
return;
82-
}
83-
8477
var selectedValue = currentTrack.Context.SelectedValues[0];
8578
var sourceUrl = await GetSourceUrl(selectedValue, radioSourceService, youTubeService, currentTrack);
8679

@@ -113,7 +106,7 @@ await playerState.CurrentVoiceClient.EnterSpeakingStateAsync(
113106
async Task DisconnectVoiceClientAsync()
114107
{
115108
await client.UpdateVoiceStateAsync(
116-
new VoiceStateProperties(Context.Guild!.Id, null),
109+
new VoiceStateProperties(currentTrack.Context.Guild!.Id, null),
117110
null,
118111
playerState.StopCts?.Token ?? CancellationToken.None);
119112
await playerState.CurrentVoiceClient.CloseAsync(
@@ -160,19 +153,19 @@ private async Task<string> GetSourceUrl(string selectedValue, IRadioSourceServic
160153
playerState.SkipCts?.Token ?? CancellationToken.None),
161154
UserId = currentTrack.Context.User.Id
162155
};
163-
ffmpegProcessService.OnProcessStart += () => HandleOnProcessStartAsync(song);
156+
ffmpegProcessService.OnProcessStart += () => HandleOnProcessStartAsync(song, currentTrack.Context);
164157
}
165158

166159
return sourceUrl;
167160
}
168161

169-
private async Task HandleOnProcessStartAsync(SongDtoBase song)
162+
private async Task HandleOnProcessStartAsync(SongDtoBase song, StringMenuInteractionContext currentContext)
170163
{
171164
using var scope = serviceProvider.CreateScope();
172165
var statisticsService = scope.ServiceProvider
173166
.GetRequiredService<IStatisticsService>();
174167
await statisticsService
175-
.LogSongPlayAsync(Context.User.Id, Context.User.Username, Context.User.GlobalName ?? string.Empty, song)
168+
.LogSongPlayAsync(currentContext.User.Id, currentContext.User.Username, currentContext.User.GlobalName ?? string.Empty, song)
176169
.ConfigureAwait(false);
177170

178171
playerState.CurrentAction = PlayerAction.Play;
@@ -181,7 +174,7 @@ await statisticsService
181174

182175
private async Task HandleOnProcessExitAsync()
183176
{
184-
if (queue.Count == 0)
177+
if (queue.Count == 0 && !_retrying)
185178
{
186179
await (DisconnectedVoiceClientEvent?.Invoke() ?? Task.CompletedTask);
187180
}
@@ -196,12 +189,17 @@ private async Task HandleFfmpegErrorAsync()
196189
{
197190
// Retry to get new stream URL and play again
198191
logger.LogWarning("Ffmpeg error received, retrying to play the stream");
192+
_retrying = true;
199193
_retryCount++;
200194
if(_retryCount > 3)
201195
{
202196
logger.LogError("Ffmpeg error received, maximum retry count reached, stopping playback");
203-
await (DisconnectedVoiceClientEvent?.Invoke() ?? Task.CompletedTask);
204-
return;
197+
if(queue.Count == 0)
198+
{
199+
await (DisconnectedVoiceClientEvent?.Invoke() ?? Task.CompletedTask);
200+
_retrying = false;
201+
return;
202+
}
205203
}
206204
await HandleMusicPlayingAsync();
207205
}

src/Infrastructure/Services/NetCordPlayerHandler.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,17 @@ private async Task HandlePlayNextAsync()
9090

9191
playerService.NotInVoiceChannelCallback += async () =>
9292
{
93-
await callbacks.Invoke();
93+
await callbacks.Invoke("You are not connected to any voice channel!");
9494
};
9595

9696
playerService.DisconnectedVoiceClientEvent += async () =>
9797
{
9898
logger.LogInformation("Voice client disconnected - stopping playback");
9999
await DisconnectVoiceClient();
100+
await callbacks.Invoke("Disconnected from voice channel either due to inactivity or error encountered.");
100101
};
101102

102-
await playerService.Play(context, SetDisconnectCallback);
103+
await playerService.Play(SetDisconnectCallback);
103104

104105
queue.DequeueAsync(CancellationToken.None);
105106
} while (!playerState.StopCts.Token.IsCancellationRequested && queue.Count > 0);

0 commit comments

Comments
 (0)