Skip to content

Commit cc87dce

Browse files
authored
Merge pull request #80 from standleypg-dev/Migrate-To-NetCord
Refine retry failing player mechanism
2 parents 2ac69cd + 98a4625 commit cc87dce

10 files changed

Lines changed: 451 additions & 198 deletions

File tree

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

3-
public record PlayRequest<TContext>(TContext Context, Func<string, Task> Callbacks);
3+
// Non-generic base type (holds members that don't depend on T)
4+
public abstract class PlayRequest
5+
{
6+
public Guid Id { get; set; } = Guid.NewGuid();
7+
public int RetryCount { get; set; }
8+
public Func<string, Task> Callbacks { get; set; } = null!;
9+
10+
public abstract object ContextAsObject { get; }
11+
}
12+
13+
public class PlayRequest<TContext> : PlayRequest
14+
{
15+
public TContext Context { get; init; } = default!;
16+
17+
public override object ContextAsObject => Context!;
18+
}

src/Application/Interfaces/Services/IMusicQueueService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ public interface IMusicQueueService
88
PlayRequest<T>? Peek<T>();
99
void DequeueAsync(CancellationToken cancellationToken);
1010
int Count { get; }
11+
PlayRequest[] GetAllRequests();
1112
}

src/Application/Interfaces/Services/INativePlaceMusicProcessorService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Application.Interfaces.Services;
55
public interface INativePlaceMusicProcessorService
66
{
77
Task<Process> CreateStreamAsync(string audioUrl, CancellationToken cancellationToken);
8-
event Func<Task>? OnExitProcess;
8+
event Func<Task>? OnPlaySongCompleted;
99
event Func<Task>? OnProcessStart;
10-
event Func<Task>? ErrorDataReceived;
10+
event Func<Task>? OnForbiddenUrlRequest;
1111
}

src/Infrastructure/Commands/NetCordCommand.cs

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,36 @@
11
using Application.Interfaces.Services;
22
using Domain.Common;
33
using Domain.Eventing;
4-
using Microsoft.Extensions.Configuration;
4+
using Domain.Events;
5+
using Infrastructure.Services;
56
using Microsoft.Extensions.DependencyInjection;
67
using NetCord.Rest;
7-
using NetCord.Services;
88
using NetCord.Services.ApplicationCommands;
99
using NetCord.Services.Commands;
10+
using NetCord.Services.ComponentInteractions;
1011
using YoutubeExplode;
1112
using YoutubeExplode.Common;
1213
using Constants = Domain.Common.Constants;
1314

1415
namespace Infrastructure.Commands;
1516

16-
public class NetCordCommand(IServiceProvider serviceProvider, IConfiguration configuration)
17+
public class NetCordCommand(IServiceProvider serviceProvider, IMusicQueueService queue)
1718
: ApplicationCommandModule<ApplicationCommandContext>
1819
{
19-
[SlashCommand("play", "Play a track from SoundCloud")]
20+
[SlashCommand("play", "Play a track from Youtube or a radio station")]
2021
public async Task PingAsync([CommandParameter(Remainder = true)] string command)
2122
{
2223
if (await NotInVoiceChannel())
2324
{
2425
return;
2526
}
27+
2628
// `AddApplicationCommands()` registers services as singleton,
2729
// so scope is needed to resolve scoped services.
2830
using var scope = serviceProvider.CreateScope();
2931
var youtubeClient = scope.ServiceProvider.GetRequiredService<YoutubeClient>();
3032
var radioSourceService = scope.ServiceProvider.GetRequiredService<IRadioSourceService>();
31-
33+
3234
var message = CreateMessage<InteractionMessageProperties>("Select a track to play:");
3335

3436
switch (command)
@@ -69,9 +71,8 @@ public async Task Stop()
6971
{
7072
return;
7173
}
72-
using var scope = serviceProvider.CreateScope();
73-
var eventDispatcher = scope.ServiceProvider.GetRequiredService<IEventDispatcher>();
74-
eventDispatcher.Dispatch(new EventType.Stop());
74+
75+
DispatchEvent(new EventType.Stop());
7576
var message = CreateMessage<InteractionMessageProperties>("Stopping playback and clearing the queue.");
7677
await RespondAsync(InteractionCallback.Message(message));
7778
}
@@ -83,13 +84,47 @@ public async Task Skip()
8384
{
8485
return;
8586
}
86-
using var scope = serviceProvider.CreateScope();
87-
var eventDispatcher = scope.ServiceProvider.GetRequiredService<IEventDispatcher>();
88-
eventDispatcher.Dispatch(new EventType.Skip());
87+
88+
DispatchEvent(new EventType.Skip());
8989
var message = CreateMessage<InteractionMessageProperties>("Skipping the current track.");
9090
await RespondAsync(InteractionCallback.Message(message));
9191
}
9292

93+
[SlashCommand("playlist", "Show the current playlist")]
94+
public async Task Playlist()
95+
{
96+
if (await NotInVoiceChannel())
97+
{
98+
return;
99+
}
100+
101+
102+
if (queue.Count == 0)
103+
await RespondAsync(InteractionCallback.Message("No songs in queue."));
104+
else
105+
{
106+
using var scope = serviceProvider.CreateScope();
107+
var youtubeService = scope.ServiceProvider.GetRequiredKeyedService<IStreamService>(nameof(YoutubeService));
108+
var songs = queue.GetAllRequests().Select(async r =>
109+
{
110+
var title = await youtubeService.GetVideoTitleAsync(
111+
(r.ContextAsObject as StringMenuInteractionContext)?.SelectedValues[0], CancellationToken.None);
112+
return title;
113+
}
114+
).ToList();
115+
116+
var titles = await Task.WhenAll(songs);
117+
118+
var response = "Queues: " + Environment.NewLine + string.Join(Environment.NewLine,
119+
titles.Select((title, index) =>
120+
{
121+
var isPlayingNowMsg = index == 0 ? "(Playing now)" : "";
122+
return $"{index + 1}. {title} {isPlayingNowMsg}";
123+
}));
124+
await RespondAsync(InteractionCallback.Message(response));
125+
}
126+
}
127+
93128
private static T CreateMessage<T>(string message) where T : IMessageProperties, new()
94129
{
95130
return new()
@@ -113,7 +148,7 @@ private static IEnumerable<IMessageComponentProperties> CreateComponent<T>(T sou
113148
}
114149
];
115150
}
116-
151+
117152
private async Task<bool> NotInVoiceChannel()
118153
{
119154
if (!Context.Guild!.VoiceStates.TryGetValue(Context.User.Id, out _))
@@ -127,5 +162,13 @@ private async Task<bool> NotInVoiceChannel()
127162
return false;
128163
}
129164

165+
private void DispatchEvent<TEvent>(TEvent @event) where TEvent : IEvent
166+
{
167+
using var scope = serviceProvider.CreateScope();
168+
var eventDispatcher = scope.ServiceProvider.GetRequiredService<IEventDispatcher>();
169+
170+
eventDispatcher.Dispatch(@event);
171+
}
172+
130173
private record ComponentModel(string Title, string Url, string? Description = null);
131174
}

src/Infrastructure/Interaction/NetCordInteraction.cs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ public class NetCordInteraction(
1919
[ComponentInteraction(Constants.CustomIds.Play)]
2020
public async Task<string> Play()
2121
{
22-
var context = Context;
22+
if (!CheckMessageExpiration())
23+
{
24+
return "This interaction has expired. Please use the play command again.";
25+
}
26+
2327
if (!NotInVoiceChannel())
2428
{
2529
return "You must be in a voice channel to use this command.";
@@ -33,7 +37,11 @@ public async Task<string> Play()
3337
logger.LogInformation("Play command invoked by user {UserId} in guild {GuildId}", Context.User.Id,
3438
Context.Guild?.Id);
3539

36-
var playRequest = new PlayRequest<StringMenuInteractionContext>(Context, RespondAsyncCallback);
40+
var playRequest = new PlayRequest<StringMenuInteractionContext>
41+
{
42+
Context = Context,
43+
Callbacks = async message => await RespondAsyncCallback(message),
44+
};
3745
queueService.Enqueue(playRequest);
3846
eventDispatcher.Dispatch(new EventType.Play());
3947
var selectedValue = Context.SelectedValues[0];
@@ -64,15 +72,12 @@ private bool NotDeafened()
6472
return !(voiceState.IsDeafened || voiceState.IsSelfDeafened);
6573
}
6674

67-
private bool UserAndBotInSameVoiceChannel()
75+
private bool CheckMessageExpiration()
6876
{
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;
77+
var messageCreationTime = Context.Message.CreatedAt;
78+
var interactionTime = Context.Interaction.CreatedAt;
79+
var timeDifference = interactionTime - messageCreationTime;
80+
return timeDifference.TotalMinutes <= 15;
7681
}
7782

7883
private Task<InteractionCallbackResponse> RespondAsyncCallback(string message) => RespondAsync(InteractionCallback.Message(message))!;

0 commit comments

Comments
 (0)