diff --git a/src/Lavalink4NET.Tests/Players/LavalinkPlayerTests.cs b/src/Lavalink4NET.Tests/Players/LavalinkPlayerTests.cs index b227e530..641438ab 100644 --- a/src/Lavalink4NET.Tests/Players/LavalinkPlayerTests.cs +++ b/src/Lavalink4NET.Tests/Players/LavalinkPlayerTests.cs @@ -16,7 +16,6 @@ using Lavalink4NET.Protocol.Requests; using Lavalink4NET.Rest; using Lavalink4NET.Rest.Entities.Tracks; -using Lavalink4NET.Tracks; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -913,25 +912,25 @@ protected override ValueTask NotifyFiltersUpdatedAsync(IPlayerFilters filters, C return base.NotifyFiltersUpdatedAsync(filters, cancellationToken); } - protected override ValueTask NotifyTrackEndedAsync(LavalinkTrack track, TrackEndReason endReason, CancellationToken cancellationToken = default) + protected override ValueTask NotifyTrackEndedAsync(ITrackQueueItem track, TrackEndReason endReason, CancellationToken cancellationToken = default) { TriggeredEvents.Add(nameof(NotifyTrackEndedAsync)); return base.NotifyTrackEndedAsync(track, endReason, cancellationToken); } - protected override ValueTask NotifyTrackExceptionAsync(LavalinkTrack track, TrackException exception, CancellationToken cancellationToken = default) + protected override ValueTask NotifyTrackExceptionAsync(ITrackQueueItem track, TrackException exception, CancellationToken cancellationToken = default) { TriggeredEvents.Add(nameof(NotifyTrackExceptionAsync)); return base.NotifyTrackExceptionAsync(track, exception, cancellationToken); } - protected override ValueTask NotifyTrackStartedAsync(LavalinkTrack track, CancellationToken cancellationToken = default) + protected override ValueTask NotifyTrackStartedAsync(ITrackQueueItem track, CancellationToken cancellationToken = default) { TriggeredEvents.Add(nameof(NotifyTrackStartedAsync)); return base.NotifyTrackStartedAsync(track, cancellationToken); } - protected override ValueTask NotifyTrackStuckAsync(LavalinkTrack track, TimeSpan threshold, CancellationToken cancellationToken = default) + protected override ValueTask NotifyTrackStuckAsync(ITrackQueueItem track, TimeSpan threshold, CancellationToken cancellationToken = default) { TriggeredEvents.Add(nameof(NotifyTrackStuckAsync)); return base.NotifyTrackStuckAsync(track, threshold, cancellationToken); diff --git a/src/Lavalink4NET/Players/Queued/ITrackQueueItem.cs b/src/Lavalink4NET/Players/ITrackQueueItem.cs similarity index 91% rename from src/Lavalink4NET/Players/Queued/ITrackQueueItem.cs rename to src/Lavalink4NET/Players/ITrackQueueItem.cs index fe34cdb5..3177b764 100644 --- a/src/Lavalink4NET/Players/Queued/ITrackQueueItem.cs +++ b/src/Lavalink4NET/Players/ITrackQueueItem.cs @@ -1,4 +1,4 @@ -namespace Lavalink4NET.Players.Queued; +namespace Lavalink4NET.Players; using Lavalink4NET.Tracks; diff --git a/src/Lavalink4NET/Players/LavalinkPlayer.cs b/src/Lavalink4NET/Players/LavalinkPlayer.cs index 1eadc381..60911f27 100644 --- a/src/Lavalink4NET/Players/LavalinkPlayer.cs +++ b/src/Lavalink4NET/Players/LavalinkPlayer.cs @@ -2,11 +2,13 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Lavalink4NET.Clients; +using Lavalink4NET.Players.Queued; using Lavalink4NET.Protocol; using Lavalink4NET.Protocol.Models; using Lavalink4NET.Protocol.Models.Filters; @@ -31,6 +33,8 @@ public class LavalinkPlayer : ILavalinkPlayer, ILavalinkPlayerListener private bool _disconnectOnDestroy; private bool _connectedOnce; private ulong _trackVersion; + private ITrackQueueItem? _nextTrack; + private ITrackQueueItem? _skippedTrack; public LavalinkPlayer(IPlayerProperties properties) { @@ -72,6 +76,10 @@ public LavalinkPlayer(IPlayerProperties p State = PlayerState.Playing; } + _nextTrack = CurrentTrack is not null + ? new TrackQueueItem(new TrackReference(CurrentTrack)) + : null; + Refresh(properties.InitialState); } @@ -118,7 +126,9 @@ public TrackPosition? Position public string Label { get; } - public LavalinkTrack? CurrentTrack { get; private set; } + public LavalinkTrack? CurrentTrack => CurrentItem?.Track; + + public ITrackQueueItem? CurrentItem { get; protected internal set; } private async ValueTask NotifyChannelUpdateCoreAsync(ulong? voiceChannelId, CancellationToken cancellationToken) { @@ -152,19 +162,20 @@ async ValueTask ILavalinkPlayerListener.NotifyTrackEndedAsync(LavalinkTrack trac cancellationToken.ThrowIfCancellationRequested(); ArgumentNullException.ThrowIfNull(track); + Debug.Assert(State is PlayerState.Playing); + var currentTrackVersion = _trackVersion; - var previousTrack = CurrentTrack; + var previousItem = Interlocked.Exchange(ref _skippedTrack, null) ?? ResolveTrackQueueItem(track); try { - await NotifyTrackEndedAsync(track, endReason, cancellationToken).ConfigureAwait(false); + await NotifyTrackEndedAsync(previousItem, endReason, cancellationToken).ConfigureAwait(false); } finally { if (Volatile.Read(ref _trackVersion) == currentTrackVersion) { - Debug.Assert(ReferenceEquals(previousTrack, CurrentTrack)); - CurrentTrack = null; + CurrentItem = null; await UpdateStateAsync(PlayerState.NotPlaying, cancellationToken).ConfigureAwait(false); } } @@ -174,7 +185,7 @@ ValueTask ILavalinkPlayerListener.NotifyTrackExceptionAsync(LavalinkTrack track, { cancellationToken.ThrowIfCancellationRequested(); ArgumentNullException.ThrowIfNull(track); - return NotifyTrackExceptionAsync(track, exception, cancellationToken); + return NotifyTrackExceptionAsync(ResolveTrackQueueItem(track), exception, cancellationToken); } ValueTask ILavalinkPlayerListener.NotifyTrackStartedAsync(LavalinkTrack track, CancellationToken cancellationToken) @@ -182,17 +193,22 @@ ValueTask ILavalinkPlayerListener.NotifyTrackStartedAsync(LavalinkTrack track, C cancellationToken.ThrowIfCancellationRequested(); ArgumentNullException.ThrowIfNull(track); - CurrentTrack = track; + var nextTrack = Interlocked.Exchange(ref _nextTrack, null) ?? CurrentItem; + + CurrentItem = track.Identifier == nextTrack?.Identifier + ? nextTrack + : new TrackQueueItem(new TrackReference(track)); + Interlocked.Increment(ref _trackVersion); - return NotifyTrackStartedAsync(track, cancellationToken); + return NotifyTrackStartedAsync(ResolveTrackQueueItem(track), cancellationToken); } ValueTask ILavalinkPlayerListener.NotifyTrackStuckAsync(LavalinkTrack track, TimeSpan threshold, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ArgumentNullException.ThrowIfNull(track); - return NotifyTrackStuckAsync(track, threshold, cancellationToken); + return NotifyTrackStuckAsync(ResolveTrackQueueItem(track), threshold, cancellationToken); } public virtual async ValueTask PauseAsync(CancellationToken cancellationToken = default) @@ -206,32 +222,19 @@ public virtual async ValueTask PauseAsync(CancellationToken cancellationToken = _logger.PlayerPaused(Label); } - public ValueTask PlayAsync(LavalinkTrack track, TrackPlayProperties properties = default, CancellationToken cancellationToken = default) + public virtual async ValueTask PlayAsync(ITrackQueueItem trackQueueItem, TrackPlayProperties properties = default, CancellationToken cancellationToken = default) { EnsureNotDestroyed(); cancellationToken.ThrowIfCancellationRequested(); - return PlayAsync(new TrackReference(track), properties, cancellationToken); - } - - public ValueTask PlayAsync(string identifier, TrackPlayProperties properties = default, CancellationToken cancellationToken = default) - { - EnsureNotDestroyed(); - cancellationToken.ThrowIfCancellationRequested(); - - return PlayAsync(new TrackReference(identifier), properties, cancellationToken); - } - - public virtual async ValueTask PlayAsync(TrackReference trackReference, TrackPlayProperties properties = default, CancellationToken cancellationToken = default) - { - EnsureNotDestroyed(); - cancellationToken.ThrowIfCancellationRequested(); + _skippedTrack = CurrentItem; + CurrentItem = _nextTrack = trackQueueItem; var updateProperties = new PlayerUpdateProperties(); - if (trackReference.IsPresent) + if (trackQueueItem.Reference.IsPresent) { - var playableTrack = await trackReference.Track + var playableTrack = await trackQueueItem.Reference.Track .GetPlayableTrackAsync(cancellationToken) .ConfigureAwait(false); @@ -239,7 +242,7 @@ public virtual async ValueTask PlayAsync(TrackReference trackReference, TrackPla } else { - updateProperties.Identifier = trackReference.Identifier; + updateProperties.Identifier = trackQueueItem.Reference.Identifier; } if (properties.StartPosition is not null) @@ -255,6 +258,30 @@ public virtual async ValueTask PlayAsync(TrackReference trackReference, TrackPla await PerformUpdateAsync(updateProperties, cancellationToken).ConfigureAwait(false); } + public ValueTask PlayAsync(LavalinkTrack track, TrackPlayProperties properties = default, CancellationToken cancellationToken = default) + { + EnsureNotDestroyed(); + cancellationToken.ThrowIfCancellationRequested(); + + return PlayAsync(new TrackQueueItem(new TrackReference(track)), properties, cancellationToken); + } + + public ValueTask PlayAsync(string identifier, TrackPlayProperties properties = default, CancellationToken cancellationToken = default) + { + EnsureNotDestroyed(); + cancellationToken.ThrowIfCancellationRequested(); + + return PlayAsync(new TrackQueueItem(new TrackReference(identifier)), properties, cancellationToken); + } + + public ValueTask PlayAsync(TrackReference trackReference, TrackPlayProperties properties = default, CancellationToken cancellationToken = default) + { + EnsureNotDestroyed(); + cancellationToken.ThrowIfCancellationRequested(); + + return PlayAsync(new TrackQueueItem(trackReference), properties, cancellationToken); + } + public async ValueTask RefreshAsync(CancellationToken cancellationToken = default) { EnsureNotDestroyed(); @@ -355,15 +382,15 @@ protected void EnsureNotDestroyed() protected virtual ValueTask NotifyWebSocketClosedAsync(WebSocketCloseStatus closeStatus, string reason, bool byRemote = false, CancellationToken cancellationToken = default) => default; - protected virtual ValueTask NotifyTrackEndedAsync(LavalinkTrack track, TrackEndReason endReason, CancellationToken cancellationToken = default) => default; + protected virtual ValueTask NotifyTrackEndedAsync(ITrackQueueItem track, TrackEndReason endReason, CancellationToken cancellationToken = default) => default; protected virtual ValueTask NotifyChannelUpdateAsync(ulong? voiceChannelId, CancellationToken cancellationToken = default) => default; - protected virtual ValueTask NotifyTrackExceptionAsync(LavalinkTrack track, TrackException exception, CancellationToken cancellationToken = default) => default; + protected virtual ValueTask NotifyTrackExceptionAsync(ITrackQueueItem track, TrackException exception, CancellationToken cancellationToken = default) => default; - protected virtual ValueTask NotifyTrackStartedAsync(LavalinkTrack track, CancellationToken cancellationToken = default) => default; + protected virtual ValueTask NotifyTrackStartedAsync(ITrackQueueItem track, CancellationToken cancellationToken = default) => default; - protected virtual ValueTask NotifyTrackStuckAsync(LavalinkTrack track, TimeSpan threshold, CancellationToken cancellationToken = default) => default; + protected virtual ValueTask NotifyTrackStuckAsync(ITrackQueueItem track, TimeSpan threshold, CancellationToken cancellationToken = default) => default; protected virtual ValueTask NotifyFiltersUpdatedAsync(IPlayerFilters filters, CancellationToken cancellationToken = default) => default; @@ -401,15 +428,15 @@ private void Refresh(PlayerInformationModel model) if (model.CurrentTrack is null) { _currentTrackState = null; - CurrentTrack = null; + CurrentItem = null; } - else + else if (model.CurrentTrack.Information.Identifier != CurrentItem?.Track?.Identifier) { _currentTrackState = model.CurrentTrack.Data; var track = model.CurrentTrack.Information; - CurrentTrack = new LavalinkTrack + var currentTrack = new LavalinkTrack { Author = track.Author, Identifier = track.Identifier, @@ -425,6 +452,8 @@ private void Refresh(PlayerInformationModel model) TrackData = model.CurrentTrack.Data, AdditionalInformation = model.CurrentTrack.AdditionalInformation, }; + + CurrentItem = new TrackQueueItem(new TrackReference(currentTrack)); } Interlocked.Increment(ref _trackVersion); @@ -613,6 +642,25 @@ private ValueTask UpdateVoiceCredentialsAsync(CancellationToken cancellationToke return PerformUpdateAsync(properties, cancellationToken); } + + [return: NotNullIfNotNull(nameof(track))] + private ITrackQueueItem? ResolveTrackQueueItem(LavalinkTrack? track) + { + if (track is null) + { + Debug.Assert(CurrentItem is null); + return null; + } + + Debug.Assert(track.Identifier == CurrentItem?.Track?.Identifier); + + if (track.Identifier == CurrentItem?.Track?.Identifier) + { + return CurrentItem; + } + + return new TrackQueueItem(new TrackReference(track)); + } } internal static partial class Logging diff --git a/src/Lavalink4NET/Players/Queued/IQueuedLavalinkPlayer.cs b/src/Lavalink4NET/Players/Queued/IQueuedLavalinkPlayer.cs index a13f1fcf..814eda83 100644 --- a/src/Lavalink4NET/Players/Queued/IQueuedLavalinkPlayer.cs +++ b/src/Lavalink4NET/Players/Queued/IQueuedLavalinkPlayer.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; +using Lavalink4NET.Players; using Lavalink4NET.Tracks; public interface IQueuedLavalinkPlayer : ILavalinkPlayer diff --git a/src/Lavalink4NET/Players/Queued/ITrackCollection.cs b/src/Lavalink4NET/Players/Queued/ITrackCollection.cs index c7a156c4..303b4fe3 100644 --- a/src/Lavalink4NET/Players/Queued/ITrackCollection.cs +++ b/src/Lavalink4NET/Players/Queued/ITrackCollection.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Lavalink4NET.Players; public interface ITrackCollection : IReadOnlyList { diff --git a/src/Lavalink4NET/Players/Queued/ITrackQueue.cs b/src/Lavalink4NET/Players/Queued/ITrackQueue.cs index 11c9db8e..ddbfdad1 100644 --- a/src/Lavalink4NET/Players/Queued/ITrackQueue.cs +++ b/src/Lavalink4NET/Players/Queued/ITrackQueue.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; +using Lavalink4NET.Players; public interface ITrackQueue : ITrackCollection { diff --git a/src/Lavalink4NET/Players/Queued/QueuedLavalinkPlayer.cs b/src/Lavalink4NET/Players/Queued/QueuedLavalinkPlayer.cs index f6413c5c..64c51016 100644 --- a/src/Lavalink4NET/Players/Queued/QueuedLavalinkPlayer.cs +++ b/src/Lavalink4NET/Players/Queued/QueuedLavalinkPlayer.cs @@ -1,10 +1,10 @@ namespace Lavalink4NET.Players.Queued; using System; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Lavalink4NET.Extensions; +using Lavalink4NET.Players; using Lavalink4NET.Protocol; using Lavalink4NET.Protocol.Payloads.Events; using Lavalink4NET.Tracks; @@ -20,8 +20,6 @@ public class QueuedLavalinkPlayer : LavalinkPlayer, IQueuedLavalinkPlayer private readonly bool _resetShuffleOnStop; private readonly bool _respectTrackRepeatOnSkip; private readonly TrackRepeatMode _defaultTrackRepeatMode; - private ITrackQueueItem? _nextTrackQueueItem; - private ITrackQueueItem? _stoppedTrackQueueItem; /// /// Initializes a new instance of the class. @@ -43,14 +41,8 @@ public QueuedLavalinkPlayer(IPlayerProperties /// Gets the track queue. /// @@ -84,13 +76,9 @@ public async ValueTask PlayAsync(ITrackQueueItem queueItem, bool enqueue = return position; } - _nextTrackQueueItem = queueItem; - CurrentItem ??= queueItem; - _stoppedTrackQueueItem = null; - // play the track immediately await base - .PlayAsync(queueItem.Reference, properties, cancellationToken) + .PlayAsync(queueItem, properties, cancellationToken) .ConfigureAwait(false); // 0 = now playing @@ -101,6 +89,7 @@ public ValueTask PlayAsync(LavalinkTrack track, bool enqueue = true, TrackP { EnsureNotDestroyed(); cancellationToken.ThrowIfCancellationRequested(); + ArgumentNullException.ThrowIfNull(track); return PlayAsync(new TrackReference(track), enqueue, properties, cancellationToken); } @@ -109,6 +98,7 @@ public ValueTask PlayAsync(string identifier, bool enqueue = true, TrackPla { EnsureNotDestroyed(); cancellationToken.ThrowIfCancellationRequested(); + ArgumentNullException.ThrowIfNull(identifier); return PlayAsync(new TrackReference(identifier), enqueue, properties, cancellationToken); } @@ -121,10 +111,12 @@ public ValueTask PlayAsync(TrackReference trackReference, bool enqueue = tr return PlayAsync(new TrackQueueItem(trackReference), enqueue, properties, cancellationToken); } - public override async ValueTask PlayAsync(TrackReference trackReference, TrackPlayProperties properties = default, CancellationToken cancellationToken = default) + public override ValueTask PlayAsync(ITrackQueueItem trackQueueItem, TrackPlayProperties properties = default, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - await PlayAsync(trackReference, enqueue: true, properties, cancellationToken).ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(trackQueueItem); + + return new ValueTask(PlayAsync(trackQueueItem, enqueue: true, properties, cancellationToken).AsTask()); } /// @@ -178,9 +170,6 @@ await Queue.History Shuffle = false; } - _stoppedTrackQueueItem = CurrentItem; - CurrentItem = null; - await base .StopAsync(cancellationToken) .ConfigureAwait(false); @@ -193,7 +182,7 @@ protected virtual ValueTask NotifyTrackEnqueuedAsync(ITrackQueueItem queueItem, return default; } - protected virtual async ValueTask NotifyTrackEndedAsync(ITrackQueueItem queueItem, TrackEndReason endReason, CancellationToken cancellationToken = default) + protected override async ValueTask NotifyTrackEndedAsync(ITrackQueueItem queueItem, TrackEndReason endReason, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ArgumentNullException.ThrowIfNull(queueItem); @@ -207,7 +196,7 @@ await Queue.History } await base - .NotifyTrackEndedAsync(queueItem.Track!, endReason, cancellationToken) + .NotifyTrackEndedAsync(queueItem, endReason, cancellationToken) .ConfigureAwait(false); if (endReason.MayStartNext()) @@ -220,22 +209,6 @@ await base } } - protected sealed override ValueTask NotifyTrackEndedAsync(LavalinkTrack track, TrackEndReason endReason, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - ArgumentNullException.ThrowIfNull(track); - - var currentItem = Interlocked.Exchange(ref _stoppedTrackQueueItem, null) ?? CurrentItem; - - Debug.Assert(currentItem?.Track?.Identifier == track.Identifier); - - var queueItem = currentItem?.Track?.Identifier == track.Identifier - ? currentItem - : new TrackQueueItem(new TrackReference(track)); - - return NotifyTrackEndedAsync(queueItem, endReason, cancellationToken); - } - private async ValueTask PlayNextAsync(int skipCount = 1, bool respectTrackRepeat = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -250,39 +223,8 @@ private async ValueTask PlayNextAsync(int skipCount = 1, bool respectTrackRepeat return; } - _nextTrackQueueItem = track.Value; - _stoppedTrackQueueItem = CurrentItem; - CurrentItem = track.Value; - - await base - .PlayAsync(track.Value.Reference, properties: default, cancellationToken) - .ConfigureAwait(false); - } - - protected sealed override ValueTask NotifyTrackStartedAsync(LavalinkTrack track, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - ArgumentNullException.ThrowIfNull(track); - - Debug.Assert(_nextTrackQueueItem?.Track?.Identifier == track.Identifier); - - var queueItem = _nextTrackQueueItem?.Track?.Identifier == track.Identifier - ? _nextTrackQueueItem - : new TrackQueueItem(new TrackReference(track)); - - CurrentItem = queueItem; - _nextTrackQueueItem = null; - - return NotifyTrackStartedAsync(queueItem, cancellationToken); - } - - protected virtual async ValueTask NotifyTrackStartedAsync(ITrackQueueItem queueItem, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - ArgumentNullException.ThrowIfNull(queueItem); - await base - .NotifyTrackStartedAsync(queueItem.Track!, cancellationToken) + .PlayAsync(track.Value, properties: default, cancellationToken) .ConfigureAwait(false); } diff --git a/src/Lavalink4NET/Players/Queued/TrackCollection.cs b/src/Lavalink4NET/Players/Queued/TrackCollection.cs index 4d220918..cd08fabb 100644 --- a/src/Lavalink4NET/Players/Queued/TrackCollection.cs +++ b/src/Lavalink4NET/Players/Queued/TrackCollection.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Lavalink4NET.Players; public abstract class TrackCollection : ITrackCollection { diff --git a/src/Lavalink4NET/Players/Queued/TrackHistory.cs b/src/Lavalink4NET/Players/Queued/TrackHistory.cs index c197cf0e..208fca57 100644 --- a/src/Lavalink4NET/Players/Queued/TrackHistory.cs +++ b/src/Lavalink4NET/Players/Queued/TrackHistory.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; +using Lavalink4NET.Players; public sealed class TrackHistory : TrackCollection, ITrackHistory { diff --git a/src/Lavalink4NET/Players/Queued/TrackQueue.cs b/src/Lavalink4NET/Players/Queued/TrackQueue.cs index dd9e56a2..219217da 100644 --- a/src/Lavalink4NET/Players/Queued/TrackQueue.cs +++ b/src/Lavalink4NET/Players/Queued/TrackQueue.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Lavalink4NET.Players; public class TrackQueue : TrackCollection, ITrackQueue { diff --git a/src/Lavalink4NET/Players/Vote/VoteLavalinkPlayer.cs b/src/Lavalink4NET/Players/Vote/VoteLavalinkPlayer.cs index 6f29adb5..41e13091 100644 --- a/src/Lavalink4NET/Players/Vote/VoteLavalinkPlayer.cs +++ b/src/Lavalink4NET/Players/Vote/VoteLavalinkPlayer.cs @@ -6,6 +6,7 @@ namespace Lavalink4NET.Players.Vote; using System.Threading; using System.Threading.Tasks; using Lavalink4NET.Clients; +using Lavalink4NET.Players; using Lavalink4NET.Players.Queued; using Lavalink4NET.Protocol.Payloads.Events; using Microsoft.Extensions.Internal;