diff --git a/DisCatSharp.Common/GlobalSuppressions.cs b/DisCatSharp.Common/GlobalSuppressions.cs index cb38bcb49..1839745ae 100644 --- a/DisCatSharp.Common/GlobalSuppressions.cs +++ b/DisCatSharp.Common/GlobalSuppressions.cs @@ -1,76 +1,69 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Common.Utilities.AsyncEvent`2._lock")] [assembly: SuppressMessage("Style", "IDE0022:Use expression body for methods", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.CharSpanLookupDictionary`1.Enumerator.Dispose")] [assembly: SuppressMessage("Style", "IDE0083:Use pattern matching", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.CharSpanLookupDictionary`1.System#Collections#IDictionary#Add(System.Object,System.Object)")] [assembly: SuppressMessage("Style", "IDE0083:Use pattern matching", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.CharSpanLookupDictionary`1.System#Collections#IDictionary#Contains(System.Object)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.CharSpanLookupDictionary`1.System#Collections#IDictionary#Contains(System.Object)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0083:Use pattern matching", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.CharSpanLookupDictionary`1.System#Collections#IDictionary#Remove(System.Object)")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.CharSpanLookupDictionary`1.TryGetValue(System.String,`0@)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.CharSpanLookupDictionary`1.TryRemove(System.String,`0@)~System.Boolean")] -[assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Extensions.FirstTwoOrDefault``1(System.Collections.Generic.IEnumerable{``0})~System.ValueTuple{``0,``0}")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Extensions.IsInRange(System.Double,System.Double,System.Double,System.Boolean)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Extensions.IsInRange(System.Single,System.Single,System.Single,System.Boolean)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0047:Remove unnecessary parentheses", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Types.ContinuousMemoryBuffer`1.Read(System.Span{`0},System.UInt64,System.Int32@)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Types.ContinuousMemoryBuffer`1.ToArray~`0[]")] [assembly: SuppressMessage("Style", "IDE0022:Use expression body for methods", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Utilities.AsyncEvent`2.UnregisterAll")] [assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Common.CharSpanLookupDictionary`1.Enumerator.System#Collections#IDictionaryEnumerator#Entry")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Common.CharSpanLookupDictionary`1.Item(System.ReadOnlySpan{System.Char})")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Common.CharSpanLookupDictionary`1.Item(System.String)")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Common.CharSpanLookupDictionary`1.System#Collections#IDictionary#Item(System.Object)")] [assembly: SuppressMessage("Style", "IDE0083:Use pattern matching", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Common.CharSpanLookupDictionary`1.System#Collections#IDictionary#Item(System.Object)")] [assembly: SuppressMessage("Style", "IDE0022:Use expression body for methods", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.CharSpanLookupReadOnlyDictionary`1.Enumerator.Dispose")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.CharSpanLookupReadOnlyDictionary`1.TryGetValue(System.String,`0@)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0022:Use expression body for methods", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetBytes(System.Span{System.Byte})")] [assembly: SuppressMessage("Style", "IDE0022:Use expression body for methods", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetNonZeroBytes(System.Span{System.Byte})")] [assembly: SuppressMessage("Style", "IDE0047:Remove unnecessary parentheses", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Types.MemoryBuffer`1.Grow(System.Int32)")] [assembly: SuppressMessage("Style", "IDE0047:Remove unnecessary parentheses", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Types.MemoryBuffer`1.Read(System.Span{`0},System.UInt64,System.Int32@)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Common.CharSpanLookupReadOnlyDictionary`1.Item(System.ReadOnlySpan{System.Char})")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Common.CharSpanLookupReadOnlyDictionary`1.Item(System.String)")] -[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Optional.FromDefaultValue``1~DisCatSharp.Common.Optional{``0}")] -[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Optional.FromValue``1(``0)~DisCatSharp.Common.Optional{``0}")] -[assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Optional`1.Equals(`0)~System.Boolean")] -[assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Optional`1.Equals(DisCatSharp.Common.Optional`1)~System.Boolean")] -[assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Optional`1.Equals(System.Object)~System.Boolean")] -[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Optional`1.op_Implicit(`0)~DisCatSharp.Common.Optional`1")] [assembly: SuppressMessage("Style", "IDE0022:Use expression body for methods", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetBytes(System.Byte[])")] [assembly: SuppressMessage("Style", "IDE0048:Add parentheses for clarity", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetInt16(System.Int16,System.Int16)~System.Int16")] [assembly: SuppressMessage("Style", "IDE0048:Add parentheses for clarity", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetInt32(System.Int32,System.Int32)~System.Int32")] [assembly: SuppressMessage("Style", "IDE0048:Add parentheses for clarity", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetInt64(System.Int64,System.Int64)~System.Int64")] [assembly: SuppressMessage("Style", "IDE0048:Add parentheses for clarity", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetInt8(System.SByte,System.SByte)~System.SByte")] [assembly: SuppressMessage("Style", "IDE0022:Use expression body for methods", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetNonZeroBytes(System.Byte[])")] [assembly: SuppressMessage("Style", "IDE0048:Add parentheses for clarity", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetUInt16(System.UInt16,System.UInt16)~System.UInt16")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetUInt16(System.UInt16,System.UInt16)~System.UInt16")] [assembly: SuppressMessage("Style", "IDE0048:Add parentheses for clarity", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetUInt32(System.UInt32,System.UInt32)~System.UInt32")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetUInt32(System.UInt32,System.UInt32)~System.UInt32")] [assembly: SuppressMessage("Style", "IDE0048:Add parentheses for clarity", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetUInt64(System.UInt64,System.UInt64)~System.UInt64")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetUInt64(System.UInt64,System.UInt64)~System.UInt64")] [assembly: SuppressMessage("Style", "IDE0048:Add parentheses for clarity", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetUInt8(System.Byte,System.Byte)~System.Byte")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.SecureRandom.GetUInt8(System.Byte,System.Byte)~System.Byte")] [assembly: SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Utilities.AsyncExecutor.Execute(System.Threading.Tasks.Task)")] [assembly: SuppressMessage("Style", "IDE0062:Make local function 'static'", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Utilities.AsyncExecutor.Execute(System.Threading.Tasks.Task)")] [assembly: SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Utilities.AsyncExecutor.Execute``1(System.Threading.Tasks.Task{``0})~``0")] [assembly: SuppressMessage("Style", "IDE0062:Make local function 'static'", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Utilities.AsyncExecutor.Execute``1(System.Threading.Tasks.Task{``0})~``0")] [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Common.Utilities.ReflectionUtilities.ToDictionary``1(``0)~System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object}")] diff --git a/DisCatSharp.Lavalink/LavalinkExtension.cs b/DisCatSharp.Lavalink/LavalinkExtension.cs index eeb42ef43..f14508954 100644 --- a/DisCatSharp.Lavalink/LavalinkExtension.cs +++ b/DisCatSharp.Lavalink/LavalinkExtension.cs @@ -1,216 +1,216 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Lavalink.EventArgs; using DisCatSharp.Net; namespace DisCatSharp.Lavalink { /// /// The lavalink extension. /// public sealed class LavalinkExtension : BaseExtension { /// /// Triggered whenever a node disconnects. /// public event AsyncEventHandler NodeDisconnected { add => this._nodeDisconnected.Register(value); remove => this._nodeDisconnected.Unregister(value); } private AsyncEvent _nodeDisconnected; /// /// Gets a dictionary of connected Lavalink nodes for the extension. /// public IReadOnlyDictionary ConnectedNodes { get; } private readonly ConcurrentDictionary _connectedNodes = new(); /// /// Creates a new instance of this Lavalink extension. /// internal LavalinkExtension() { this.ConnectedNodes = new ReadOnlyConcurrentDictionary(this._connectedNodes); } /// /// DO NOT USE THIS MANUALLY. /// /// DO NOT USE THIS MANUALLY. /// protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this._nodeDisconnected = new AsyncEvent("LAVALINK_NODE_DISCONNECTED", TimeSpan.Zero, this.Client.EventErrorHandler); } /// /// Connect to a Lavalink node. /// /// Lavalink client configuration. /// The established Lavalink connection. public async Task ConnectAsync(LavalinkConfiguration config) { if (this._connectedNodes.ContainsKey(config.SocketEndpoint)) return this._connectedNodes[config.SocketEndpoint]; var con = new LavalinkNodeConnection(this.Client, this, config); con.NodeDisconnected += this.Con_NodeDisconnected; con.Disconnected += this.Con_Disconnected; this._connectedNodes[con.NodeEndpoint] = con; try { await con.StartAsync().ConfigureAwait(false); } catch { this.Con_NodeDisconnected(con); throw; } return con; } /// /// Gets the Lavalink node connection for the specified endpoint. /// /// Endpoint at which the node resides. /// Lavalink node connection. public LavalinkNodeConnection GetNodeConnection(ConnectionEndpoint endpoint) => this._connectedNodes.ContainsKey(endpoint) ? this._connectedNodes[endpoint] : null; /// /// Gets a Lavalink node connection based on load balancing and an optional voice region. /// /// The region to compare with the node's , if any. /// The least load affected node connection, or null if no nodes are present. public LavalinkNodeConnection GetIdealNodeConnection(DiscordVoiceRegion region = null) { if (this._connectedNodes.Count <= 1) return this._connectedNodes.Values.FirstOrDefault(); var nodes = this._connectedNodes.Values.ToArray(); if (region != null) { var regionPredicate = new Func(x => x.Region == region); if (nodes.Any(regionPredicate)) nodes = nodes.Where(regionPredicate).ToArray(); - if (nodes.Count() <= 1) + if (nodes.Length <= 1) return nodes.FirstOrDefault(); } return this.FilterByLoad(nodes); } /// /// Gets a Lavalink guild connection from a . /// /// The guild the connection is on. /// The found guild connection, or null if one could not be found. public LavalinkGuildConnection GetGuildConnection(DiscordGuild guild) { var nodes = this._connectedNodes.Values; var node = nodes.FirstOrDefault(x => x.ConnectedGuildsInternal.ContainsKey(guild.Id)); return node?.GetGuildConnection(guild); } /// /// Filters the by load. /// /// The nodes. private LavalinkNodeConnection FilterByLoad(LavalinkNodeConnection[] nodes) { Array.Sort(nodes, (a, b) => { if (!a.Statistics.Updated || !b.Statistics.Updated) return 0; //https://github.com/FredBoat/Lavalink-Client/blob/48bc27784f57be5b95d2ff2eff6665451b9366f5/src/main/java/lavalink/client/io/LavalinkLoadBalancer.java#L122 //https://github.com/briantanner/eris-lavalink/blob/master/src/PlayerManager.js#L329 //player count var aPenaltyCount = a.Statistics.ActivePlayers; var bPenaltyCount = b.Statistics.ActivePlayers; //cpu load aPenaltyCount += (int)Math.Pow(1.05d, (100 * (a.Statistics.CpuSystemLoad / a.Statistics.CpuCoreCount) * 10) - 10); bPenaltyCount += (int)Math.Pow(1.05d, (100 * (b.Statistics.CpuSystemLoad / a.Statistics.CpuCoreCount) * 10) - 10); //frame load if (a.Statistics.AverageDeficitFramesPerMinute > 0) { //deficit frame load aPenaltyCount += (int)((Math.Pow(1.03d, 500f * (a.Statistics.AverageDeficitFramesPerMinute / 3000f)) * 600) - 600); //null frame load aPenaltyCount += (int)((Math.Pow(1.03d, 500f * (a.Statistics.AverageNulledFramesPerMinute / 3000f)) * 300) - 300); } //frame load if (b.Statistics.AverageDeficitFramesPerMinute > 0) { //deficit frame load bPenaltyCount += (int)((Math.Pow(1.03d, 500f * (b.Statistics.AverageDeficitFramesPerMinute / 3000f)) * 600) - 600); //null frame load bPenaltyCount += (int)((Math.Pow(1.03d, 500f * (b.Statistics.AverageNulledFramesPerMinute / 3000f)) * 300) - 300); } return aPenaltyCount - bPenaltyCount; }); return nodes[0]; } /// /// Removes a node. /// /// The node to be removed. private void Con_NodeDisconnected(LavalinkNodeConnection node) => this._connectedNodes.TryRemove(node.NodeEndpoint, out _); /// /// Disconnects a node. /// /// The affected node. /// The node disconnected event args. private Task Con_Disconnected(LavalinkNodeConnection node, NodeDisconnectedEventArgs e) => this._nodeDisconnected.InvokeAsync(node, e); } } diff --git a/DisCatSharp.Lavalink/LavalinkGuildConnection.cs b/DisCatSharp.Lavalink/LavalinkGuildConnection.cs index 83c24a44f..46363ffb5 100644 --- a/DisCatSharp.Lavalink/LavalinkGuildConnection.cs +++ b/DisCatSharp.Lavalink/LavalinkGuildConnection.cs @@ -1,441 +1,441 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Lavalink.Entities; using DisCatSharp.Lavalink.EventArgs; using Newtonsoft.Json; namespace DisCatSharp.Lavalink { internal delegate void ChannelDisconnectedEventHandler(LavalinkGuildConnection node); /// /// Represents a Lavalink connection to a channel. /// public sealed class LavalinkGuildConnection { /// /// Triggered whenever Lavalink updates player status. /// public event AsyncEventHandler PlayerUpdated { add => this._playerUpdated.Register(value); remove => this._playerUpdated.Unregister(value); } private readonly AsyncEvent _playerUpdated; /// /// Triggered whenever playback of a track starts. /// This is only available for version 3.3.1 and greater. /// public event AsyncEventHandler PlaybackStarted { add => this._playbackStarted.Register(value); remove => this._playbackStarted.Unregister(value); } private readonly AsyncEvent _playbackStarted; /// /// Triggered whenever playback of a track finishes. /// public event AsyncEventHandler PlaybackFinished { add => this._playbackFinished.Register(value); remove => this._playbackFinished.Unregister(value); } private readonly AsyncEvent _playbackFinished; /// /// Triggered whenever playback of a track gets stuck. /// public event AsyncEventHandler TrackStuck { add => this._trackStuck.Register(value); remove => this._trackStuck.Unregister(value); } private readonly AsyncEvent _trackStuck; /// /// Triggered whenever playback of a track encounters an error. /// public event AsyncEventHandler TrackException { add => this._trackException.Register(value); remove => this._trackException.Unregister(value); } private readonly AsyncEvent _trackException; /// /// Triggered whenever Discord Voice WebSocket connection is terminated. /// public event AsyncEventHandler DiscordWebSocketClosed { add => this._webSocketClosed.Register(value); remove => this._webSocketClosed.Unregister(value); } private readonly AsyncEvent _webSocketClosed; /// /// Gets whether this channel is still connected. /// public bool IsConnected => !Volatile.Read(ref this._isDisposed) && this.Channel != null; private bool _isDisposed; /// /// Gets the current player state. /// public LavalinkPlayerState CurrentState { get; } /// /// Gets the voice channel associated with this connection. /// public DiscordChannel Channel => this.VoiceStateUpdate.Channel; /// /// Gets the guild associated with this connection. /// public DiscordGuild Guild => this.Channel.Guild; /// /// Gets the Lavalink node associated with this connection. /// public LavalinkNodeConnection Node { get; } /// /// Gets the guild id string. /// internal string GuildIdString => this.GuildId.ToString(CultureInfo.InvariantCulture); /// /// Gets the guild id. /// internal ulong GuildId => this.Channel.Guild.Id; /// /// Gets or sets the voice state update. /// internal VoiceStateUpdateEventArgs VoiceStateUpdate { get; set; } /// /// Gets or sets the voice ws disconnect tcs. /// internal TaskCompletionSource VoiceWsDisconnectTcs { get; set; } /// /// Initializes a new instance of the class. /// /// The node. /// The channel. /// The vstu. internal LavalinkGuildConnection(LavalinkNodeConnection node, DiscordChannel channel, VoiceStateUpdateEventArgs vstu) { this.Node = node; this.VoiceStateUpdate = vstu; this.CurrentState = new LavalinkPlayerState(); this.VoiceWsDisconnectTcs = new TaskCompletionSource(); Volatile.Write(ref this._isDisposed, false); this._playerUpdated = new AsyncEvent("LAVALINK_PLAYER_UPDATE", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); this._playbackStarted = new AsyncEvent("LAVALINK_PLAYBACK_STARTED", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); this._playbackFinished = new AsyncEvent("LAVALINK_PLAYBACK_FINISHED", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); this._trackStuck = new AsyncEvent("LAVALINK_TRACK_STUCK", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); this._trackException = new AsyncEvent("LAVALINK_TRACK_EXCEPTION", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); this._webSocketClosed = new AsyncEvent("LAVALINK_DISCORD_WEBSOCKET_CLOSED", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); } /// /// Disconnects the connection from the voice channel. /// /// Whether the connection should be destroyed on the Lavalink server when leaving. public Task DisconnectAsync(bool shouldDestroy = true) => this.DisconnectInternalAsync(shouldDestroy); /// /// Disconnects the internal async. /// /// If true, should destroy. /// If true, is manual disconnection. internal async Task DisconnectInternalAsync(bool shouldDestroy, bool isManualDisconnection = false) { if (!this.IsConnected && !isManualDisconnection) throw new InvalidOperationException("This connection is not valid."); Volatile.Write(ref this._isDisposed, true); if (shouldDestroy) await this.Node.SendPayloadAsync(new LavalinkDestroy(this)).ConfigureAwait(false); if (!isManualDisconnection) { await this.SendVoiceUpdateAsync().ConfigureAwait(false); this.ChannelDisconnected?.Invoke(this); } } /// /// Sends the voice update async. /// internal async Task SendVoiceUpdateAsync() { var vsd = new VoiceDispatch { OpCode = 4, Payload = new VoiceStateUpdatePayload { GuildId = this.GuildId, ChannelId = null, Deafened = false, Muted = false } }; var vsj = JsonConvert.SerializeObject(vsd, Formatting.None); await (this.Channel.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false); } /// /// Searches for specified terms. /// /// What to search for. /// What platform will search for. /// A collection of tracks matching the criteria. public Task GetTracksAsync(string searchQuery, LavalinkSearchType type = LavalinkSearchType.Youtube) => this.Node.Rest.GetTracksAsync(searchQuery, type); /// /// Loads tracks from specified URL. /// /// URL to load tracks from. /// A collection of tracks from the URL. public Task GetTracksAsync(Uri uri) => this.Node.Rest.GetTracksAsync(uri); /// /// Loads tracks from a local file. /// /// File to load tracks from. /// A collection of tracks from the file. public Task GetTracksAsync(FileInfo file) => this.Node.Rest.GetTracksAsync(file); /// /// Queues the specified track for playback. /// /// Track to play. public async Task PlayAsync(LavalinkTrack track) { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); this.CurrentState.CurrentTrack = track; await this.Node.SendPayloadAsync(new LavalinkPlay(this, track)).ConfigureAwait(false); } /// /// Queues the specified track for playback. The track will be played from specified start timestamp to specified end timestamp. /// /// Track to play. /// Timestamp to start playback at. /// Timestamp to stop playback at. public async Task PlayPartialAsync(LavalinkTrack track, TimeSpan start, TimeSpan end) { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); if (start.TotalMilliseconds < 0 || end <= start) throw new ArgumentException("Both start and end timestamps need to be greater or equal to zero, and the end timestamp needs to be greater than start timestamp."); this.CurrentState.CurrentTrack = track; await this.Node.SendPayloadAsync(new LavalinkPlayPartial(this, track, start, end)).ConfigureAwait(false); } /// /// Stops the player completely. /// public async Task StopAsync() { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); await this.Node.SendPayloadAsync(new LavalinkStop(this)).ConfigureAwait(false); } /// /// Pauses the player. /// public async Task PauseAsync() { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); await this.Node.SendPayloadAsync(new LavalinkPause(this, true)).ConfigureAwait(false); } /// /// Resumes playback. /// public async Task ResumeAsync() { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); await this.Node.SendPayloadAsync(new LavalinkPause(this, false)).ConfigureAwait(false); } /// /// Seeks the current track to specified position. /// /// Position to seek to. public async Task SeekAsync(TimeSpan position) { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); await this.Node.SendPayloadAsync(new LavalinkSeek(this, position)).ConfigureAwait(false); } /// /// Sets the playback volume. This might incur a lot of CPU usage. /// /// Volume to set. Needs to be greater or equal to 0, and less than or equal to 100. 100 means 100% and is the default value. public async Task SetVolumeAsync(int volume) { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); if (volume < 0 || volume > 1000) throw new ArgumentOutOfRangeException(nameof(volume), "Volume needs to range from 0 to 1000."); await this.Node.SendPayloadAsync(new LavalinkVolume(this, volume)).ConfigureAwait(false); } /// /// Adjusts the specified bands in the audio equalizer. This will alter the sound output, and might incur a lot of CPU usage. /// /// Bands adjustments to make. You must specify one adjustment per band at most. public async Task AdjustEqualizerAsync(params LavalinkBandAdjustment[] bands) { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); if (bands?.Any() != true) return; - if (bands.Distinct(new LavalinkBandAdjustmentComparer()).Count() != bands.Count()) + if (bands.Distinct(new LavalinkBandAdjustmentComparer()).Count() != bands.Length) throw new InvalidOperationException("You cannot specify multiple modifiers for the same band."); await this.Node.SendPayloadAsync(new LavalinkEqualizer(this, bands)).ConfigureAwait(false); } /// /// Resets the audio equalizer to default values. /// public async Task ResetEqualizerAsync() { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); await this.Node.SendPayloadAsync(new LavalinkEqualizer(this, Enumerable.Range(0, 15).Select(x => new LavalinkBandAdjustment(x, 0)))).ConfigureAwait(false); } /// /// Internals the update player state async. /// /// The new state. internal Task InternalUpdatePlayerStateAsync(LavalinkState newState) { this.CurrentState.LastUpdate = newState.Time; this.CurrentState.PlaybackPosition = newState.Position; return this._playerUpdated.InvokeAsync(this, new PlayerUpdateEventArgs(this, newState.Time, newState.Position)); } /// /// Internals the playback started async. /// /// The track. internal Task InternalPlaybackStartedAsync(string track) { var ea = new TrackStartEventArgs(this, LavalinkUtilities.DecodeTrack(track)); return this._playbackStarted.InvokeAsync(this, ea); } /// /// Internals the playback finished async. /// /// The e. internal Task InternalPlaybackFinishedAsync(TrackFinishData e) { if (e.Reason != TrackEndReason.Replaced) this.CurrentState.CurrentTrack = default; var ea = new TrackFinishEventArgs(this, LavalinkUtilities.DecodeTrack(e.Track), e.Reason); return this._playbackFinished.InvokeAsync(this, ea); } /// /// Internals the track stuck async. /// /// The e. internal Task InternalTrackStuckAsync(TrackStuckData e) { var ea = new TrackStuckEventArgs(this, e.Threshold, LavalinkUtilities.DecodeTrack(e.Track)); return this._trackStuck.InvokeAsync(this, ea); } /// /// Internals the track exception async. /// /// The e. internal Task InternalTrackExceptionAsync(TrackExceptionData e) { var ea = new TrackExceptionEventArgs(this, e.Error, LavalinkUtilities.DecodeTrack(e.Track)); return this._trackException.InvokeAsync(this, ea); } /// /// Internals the web socket closed async. /// /// The e. internal Task InternalWebSocketClosedAsync(WebSocketCloseEventArgs e) => this._webSocketClosed.InvokeAsync(this, e); internal event ChannelDisconnectedEventHandler ChannelDisconnected; } } diff --git a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs index 864a7c290..8c4cd83c8 100644 --- a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs +++ b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs @@ -1,609 +1,609 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Lavalink.Entities; using DisCatSharp.Lavalink.EventArgs; using DisCatSharp.Net; using DisCatSharp.Net.WebSocket; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Lavalink { internal delegate void NodeDisconnectedEventHandler(LavalinkNodeConnection node); /// /// Represents a connection to a Lavalink node. /// public sealed class LavalinkNodeConnection { /// /// Triggered whenever Lavalink WebSocket throws an exception. /// public event AsyncEventHandler LavalinkSocketErrored { add => this._lavalinkSocketError.Register(value); remove => this._lavalinkSocketError.Unregister(value); } private readonly AsyncEvent _lavalinkSocketError; /// /// Triggered when this node disconnects. /// public event AsyncEventHandler Disconnected { add => this._disconnected.Register(value); remove => this._disconnected.Unregister(value); } private readonly AsyncEvent _disconnected; /// /// Triggered when this node receives a statistics update. /// public event AsyncEventHandler StatisticsReceived { add => this._statsReceived.Register(value); remove => this._statsReceived.Unregister(value); } private readonly AsyncEvent _statsReceived; /// /// Triggered whenever any of the players on this node is updated. /// public event AsyncEventHandler PlayerUpdated { add => this._playerUpdated.Register(value); remove => this._playerUpdated.Unregister(value); } private readonly AsyncEvent _playerUpdated; /// /// Triggered whenever playback of a track starts. /// This is only available for version 3.3.1 and greater. /// public event AsyncEventHandler PlaybackStarted { add => this._playbackStarted.Register(value); remove => this._playbackStarted.Unregister(value); } private readonly AsyncEvent _playbackStarted; /// /// Triggered whenever playback of a track finishes. /// public event AsyncEventHandler PlaybackFinished { add => this._playbackFinished.Register(value); remove => this._playbackFinished.Unregister(value); } private readonly AsyncEvent _playbackFinished; /// /// Triggered whenever playback of a track gets stuck. /// public event AsyncEventHandler TrackStuck { add => this._trackStuck.Register(value); remove => this._trackStuck.Unregister(value); } private readonly AsyncEvent _trackStuck; /// /// Triggered whenever playback of a track encounters an error. /// public event AsyncEventHandler TrackException { add => this._trackException.Register(value); remove => this._trackException.Unregister(value); } private readonly AsyncEvent _trackException; /// /// Gets the remote endpoint of this Lavalink node connection. /// public ConnectionEndpoint NodeEndpoint => this.Configuration.SocketEndpoint; /// /// Gets whether the client is connected to Lavalink. /// public bool IsConnected => !Volatile.Read(ref this._isDisposed); private bool _isDisposed; private int _backoff; /// /// The minimum backoff. /// private const int MINIMUM_BACKOFF = 7500; /// /// The maximum backoff. /// private const int MAXIMUM_BACKOFF = 120000; /// /// Gets the current resource usage statistics. /// public LavalinkStatistics Statistics { get; } /// /// Gets a dictionary of Lavalink guild connections for this node. /// public IReadOnlyDictionary ConnectedGuilds { get; } internal ConcurrentDictionary ConnectedGuildsInternal = new(); /// /// Gets the REST client for this Lavalink connection. /// public LavalinkRestClient Rest { get; } /// /// Gets the parent extension which this node connection belongs to. /// public LavalinkExtension Parent { get; } /// /// Gets the Discord client this node connection belongs to. /// public DiscordClient Discord { get; } /// /// Gets the configuration. /// internal LavalinkConfiguration Configuration { get; } /// /// Gets the region. /// internal DiscordVoiceRegion Region { get; } /// /// Gets or sets the web socket. /// private IWebSocketClient _webSocket; /// /// Gets the voice state updates. /// private readonly ConcurrentDictionary> _voiceStateUpdates; /// /// Gets the voice server updates. /// private readonly ConcurrentDictionary> _voiceServerUpdates; /// /// Initializes a new instance of the class. /// /// The client. /// the event.tension. /// The config. internal LavalinkNodeConnection(DiscordClient client, LavalinkExtension extension, LavalinkConfiguration config) { this.Discord = client; this.Parent = extension; this.Configuration = new LavalinkConfiguration(config); if (config.Region != null && this.Discord.VoiceRegions.Values.Contains(config.Region)) this.Region = config.Region; this.ConnectedGuilds = new ReadOnlyConcurrentDictionary(this.ConnectedGuildsInternal); this.Statistics = new LavalinkStatistics(); this._lavalinkSocketError = new AsyncEvent("LAVALINK_SOCKET_ERROR", TimeSpan.Zero, this.Discord.EventErrorHandler); this._disconnected = new AsyncEvent("LAVALINK_NODE_DISCONNECTED", TimeSpan.Zero, this.Discord.EventErrorHandler); this._statsReceived = new AsyncEvent("LAVALINK_STATS_RECEIVED", TimeSpan.Zero, this.Discord.EventErrorHandler); this._playerUpdated = new AsyncEvent("LAVALINK_PLAYER_UPDATED", TimeSpan.Zero, this.Discord.EventErrorHandler); this._playbackStarted = new AsyncEvent("LAVALINK_PLAYBACK_STARTED", TimeSpan.Zero, this.Discord.EventErrorHandler); this._playbackFinished = new AsyncEvent("LAVALINK_PLAYBACK_FINISHED", TimeSpan.Zero, this.Discord.EventErrorHandler); this._trackStuck = new AsyncEvent("LAVALINK_TRACK_STUCK", TimeSpan.Zero, this.Discord.EventErrorHandler); this._trackException = new AsyncEvent("LAVALINK_TRACK_EXCEPTION", TimeSpan.Zero, this.Discord.EventErrorHandler); this._voiceServerUpdates = new ConcurrentDictionary>(); this._voiceStateUpdates = new ConcurrentDictionary>(); this.Discord.VoiceStateUpdated += this.Discord_VoiceStateUpdated; this.Discord.VoiceServerUpdated += this.Discord_VoiceServerUpdated; this.Rest = new LavalinkRestClient(this.Configuration, this.Discord); Volatile.Write(ref this._isDisposed, false); } /// /// Establishes a connection to the Lavalink node. /// /// internal async Task StartAsync() { if (this.Discord?.CurrentUser?.Id == null || this.Discord?.ShardCount == null) throw new InvalidOperationException("This operation requires the Discord client to be fully initialized."); this._webSocket = this.Discord.Configuration.WebSocketClientFactory(this.Discord.Configuration.Proxy, this.Discord.ServiceProvider); this._webSocket.Connected += this.WebSocket_OnConnect; this._webSocket.Disconnected += this.WebSocket_OnDisconnect; this._webSocket.ExceptionThrown += this.WebSocket_OnException; this._webSocket.MessageReceived += this.WebSocket_OnMessage; this._webSocket.AddDefaultHeader("Authorization", this.Configuration.Password); this._webSocket.AddDefaultHeader("Num-Shards", this.Discord.ShardCount.ToString(CultureInfo.InvariantCulture)); this._webSocket.AddDefaultHeader("User-Id", this.Discord.CurrentUser.Id.ToString(CultureInfo.InvariantCulture)); if (this.Configuration.ResumeKey != null) this._webSocket.AddDefaultHeader("Resume-Key", this.Configuration.ResumeKey); do { try { if (this._backoff != 0) { await Task.Delay(this._backoff).ConfigureAwait(false); this._backoff = Math.Min(this._backoff * 2, MAXIMUM_BACKOFF); } else { this._backoff = MINIMUM_BACKOFF; } await this._webSocket.ConnectAsync(new Uri(this.Configuration.SocketEndpoint.ToWebSocketString())).ConfigureAwait(false); break; } catch (PlatformNotSupportedException) { throw; } catch (NotImplementedException) { throw; } catch (Exception ex) { if (!this.Configuration.SocketAutoReconnect || this._backoff == MAXIMUM_BACKOFF) { this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, ex, "Failed to connect to Lavalink."); - throw ex; + throw; } else { this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, ex, $"Failed to connect to Lavalink, retrying in {this._backoff} ms."); } } } while (this.Configuration.SocketAutoReconnect); Volatile.Write(ref this._isDisposed, false); } /// /// Stops this Lavalink node connection and frees resources. /// /// public async Task StopAsync() { foreach (var kvp in this.ConnectedGuildsInternal) await kvp.Value.DisconnectAsync().ConfigureAwait(false); this.NodeDisconnected?.Invoke(this); Volatile.Write(ref this._isDisposed, true); await this._webSocket.DisconnectAsync().ConfigureAwait(false); // this should not be here, no? //await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this)).ConfigureAwait(false); } /// /// Connects this Lavalink node to specified Discord channel. /// /// Voice channel to connect to. /// Channel connection, which allows for playback control. public async Task ConnectAsync(DiscordChannel channel) { if (this.ConnectedGuildsInternal.ContainsKey(channel.Guild.Id)) return this.ConnectedGuildsInternal[channel.Guild.Id]; if (channel.Guild == null || (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage)) throw new ArgumentException("Invalid channel specified.", nameof(channel)); var vstut = new TaskCompletionSource(); var vsrut = new TaskCompletionSource(); this._voiceStateUpdates[channel.Guild.Id] = vstut; this._voiceServerUpdates[channel.Guild.Id] = vsrut; var vsd = new VoiceDispatch { OpCode = 4, Payload = new VoiceStateUpdatePayload { GuildId = channel.Guild.Id, ChannelId = channel.Id, Deafened = false, Muted = false } }; var vsj = JsonConvert.SerializeObject(vsd, Formatting.None); await (channel.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false); var vstu = await vstut.Task.ConfigureAwait(false); var vsru = await vsrut.Task.ConfigureAwait(false); await this.SendPayloadAsync(new LavalinkVoiceUpdate(vstu, vsru)).ConfigureAwait(false); var con = new LavalinkGuildConnection(this, channel, vstu); con.ChannelDisconnected += this.Con_ChannelDisconnected; con.PlayerUpdated += (s, e) => this._playerUpdated.InvokeAsync(s, e); con.PlaybackStarted += (s, e) => this._playbackStarted.InvokeAsync(s, e); con.PlaybackFinished += (s, e) => this._playbackFinished.InvokeAsync(s, e); con.TrackStuck += (s, e) => this._trackStuck.InvokeAsync(s, e); con.TrackException += (s, e) => this._trackException.InvokeAsync(s, e); this.ConnectedGuildsInternal[channel.Guild.Id] = con; return con; } /// /// Gets a Lavalink connection to specified Discord channel. /// /// Guild to get connection for. /// Channel connection, which allows for playback control. public LavalinkGuildConnection GetGuildConnection(DiscordGuild guild) => this.ConnectedGuildsInternal.TryGetValue(guild.Id, out var lgc) && lgc.IsConnected ? lgc : null; /// /// Sends the payload async. /// /// The payload. internal async Task SendPayloadAsync(LavalinkPayload payload) => await this.WsSendAsync(JsonConvert.SerializeObject(payload, Formatting.None)).ConfigureAwait(false); /// /// Webs the socket_ on message. /// /// The client. /// the event.ent. private async Task WebSocket_OnMessage(IWebSocketClient client, SocketMessageEventArgs e) { if (e is not SocketTextMessageEventArgs et) { this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, "Lavalink sent binary data - unable to process"); return; } this.Discord.Logger.LogTrace(LavalinkEvents.LavalinkWsRx, et.Message); var json = et.Message; var jsonData = JObject.Parse(json); switch (jsonData["op"].ToString()) { case "playerUpdate": var gid = (ulong)jsonData["guildId"]; var state = jsonData["state"].ToObject(); if (this.ConnectedGuildsInternal.TryGetValue(gid, out var lvl)) await lvl.InternalUpdatePlayerStateAsync(state).ConfigureAwait(false); break; case "stats": var statsRaw = jsonData.ToObject(); this.Statistics.Update(statsRaw); await this._statsReceived.InvokeAsync(this, new StatisticsReceivedEventArgs(this.Discord.ServiceProvider, this.Statistics)).ConfigureAwait(false); break; case "event": var evtype = jsonData["type"].ToObject(); var guildId = (ulong)jsonData["guildId"]; switch (evtype) { case EventType.TrackStartEvent: if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvtst)) await lvlEvtst.InternalPlaybackStartedAsync(jsonData["track"].ToString()).ConfigureAwait(false); break; case EventType.TrackEndEvent: var reason = TrackEndReason.Cleanup; switch (jsonData["reason"].ToString()) { case "FINISHED": reason = TrackEndReason.Finished; break; case "LOAD_FAILED": reason = TrackEndReason.LoadFailed; break; case "STOPPED": reason = TrackEndReason.Stopped; break; case "REPLACED": reason = TrackEndReason.Replaced; break; case "CLEANUP": reason = TrackEndReason.Cleanup; break; } if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvtf)) await lvlEvtf.InternalPlaybackFinishedAsync(new TrackFinishData { Track = jsonData["track"].ToString(), Reason = reason }).ConfigureAwait(false); break; case EventType.TrackStuckEvent: if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvts)) await lvlEvts.InternalTrackStuckAsync(new TrackStuckData { Track = jsonData["track"].ToString(), Threshold = (long)jsonData["thresholdMs"] }).ConfigureAwait(false); break; case EventType.TrackExceptionEvent: if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvte)) await lvlEvte.InternalTrackExceptionAsync(new TrackExceptionData { Track = jsonData["track"].ToString(), Error = jsonData["error"].ToString() }).ConfigureAwait(false); break; case EventType.WebSocketClosedEvent: if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEwsce)) { lvlEwsce.VoiceWsDisconnectTcs.SetResult(true); await lvlEwsce.InternalWebSocketClosedAsync(new WebSocketCloseEventArgs(jsonData["code"].ToObject(), jsonData["reason"].ToString(), jsonData["byRemote"].ToObject(), this.Discord.ServiceProvider)).ConfigureAwait(false); } break; } break; } } /// /// Webs the socket_ on exception. /// /// The client. /// the event. private Task WebSocket_OnException(IWebSocketClient client, SocketErrorEventArgs e) => this._lavalinkSocketError.InvokeAsync(this, new SocketErrorEventArgs(client.ServiceProvider) { Exception = e.Exception }); /// /// Webs the socket_ on disconnect. /// /// The client. /// the event. private async Task WebSocket_OnDisconnect(IWebSocketClient client, SocketCloseEventArgs e) { if (this.IsConnected && e.CloseCode != 1001 && e.CloseCode != -1) { this.Discord.Logger.LogWarning(LavalinkEvents.LavalinkConnectionClosed, "Connection broken ({0}, '{1}'), reconnecting", e.CloseCode, e.CloseMessage); await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, false)).ConfigureAwait(false); if (this.Configuration.SocketAutoReconnect) await this.StartAsync().ConfigureAwait(false); } else if (e.CloseCode != 1001 && e.CloseCode != -1) { this.Discord.Logger.LogInformation(LavalinkEvents.LavalinkConnectionClosed, "Connection closed ({0}, '{1}')", e.CloseCode, e.CloseMessage); this.NodeDisconnected?.Invoke(this); await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, true)).ConfigureAwait(false); } else { Volatile.Write(ref this._isDisposed, true); this.Discord.Logger.LogWarning(LavalinkEvents.LavalinkConnectionClosed, "Lavalink died"); foreach (var kvp in this.ConnectedGuildsInternal) { await kvp.Value.SendVoiceUpdateAsync().ConfigureAwait(false); _ = this.ConnectedGuildsInternal.TryRemove(kvp.Key, out _); } this.NodeDisconnected?.Invoke(this); await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, false)).ConfigureAwait(false); if (this.Configuration.SocketAutoReconnect) await this.StartAsync().ConfigureAwait(false); } } /// /// Webs the socket_ on connect. /// /// The client. /// the event.. private async Task WebSocket_OnConnect(IWebSocketClient client, SocketEventArgs ea) { this.Discord.Logger.LogDebug(LavalinkEvents.LavalinkConnected, "Connection to Lavalink node established"); this._backoff = 0; if (this.Configuration.ResumeKey != null) await this.SendPayloadAsync(new LavalinkConfigureResume(this.Configuration.ResumeKey, this.Configuration.ResumeTimeout)).ConfigureAwait(false); } /// /// Con_S the channel disconnected. /// /// The con. private void Con_ChannelDisconnected(LavalinkGuildConnection con) => this.ConnectedGuildsInternal.TryRemove(con.GuildId, out _); /// /// Discord voice state updated. /// /// The client. /// the event. private Task Discord_VoiceStateUpdated(DiscordClient client, VoiceStateUpdateEventArgs e) { var gld = e.Guild; if (gld == null) return Task.CompletedTask; if (e.User == null) return Task.CompletedTask; if (e.User.Id == this.Discord.CurrentUser.Id) { if (this.ConnectedGuildsInternal.TryGetValue(e.Guild.Id, out var lvlgc)) lvlgc.VoiceStateUpdate = e; if (e.After.Channel == null && this.IsConnected && this.ConnectedGuildsInternal.ContainsKey(gld.Id)) { _ = Task.Run(async () => { var delayTask = Task.Delay(this.Configuration.WebSocketCloseTimeout); var tcs = lvlgc.VoiceWsDisconnectTcs.Task; _ = await Task.WhenAny(delayTask, tcs).ConfigureAwait(false); await lvlgc.DisconnectInternalAsync(false, true).ConfigureAwait(false); _ = this.ConnectedGuildsInternal.TryRemove(gld.Id, out _); }); } if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this._voiceStateUpdates.TryRemove(gld.Id, out var xe)) xe.SetResult(e); } return Task.CompletedTask; } /// /// Discord voice server updated. /// /// The client. /// the event. private Task Discord_VoiceServerUpdated(DiscordClient client, VoiceServerUpdateEventArgs e) { var gld = e.Guild; if (gld == null) return Task.CompletedTask; if (this.ConnectedGuildsInternal.TryGetValue(e.Guild.Id, out var lvlgc)) { var lvlp = new LavalinkVoiceUpdate(lvlgc.VoiceStateUpdate, e); _ = Task.Run(() => this.WsSendAsync(JsonConvert.SerializeObject(lvlp))); } if (this._voiceServerUpdates.TryRemove(gld.Id, out var xe)) xe.SetResult(e); return Task.CompletedTask; } /// /// Ws the send async. /// /// The payload. private async Task WsSendAsync(string payload) { this.Discord.Logger.LogTrace(LavalinkEvents.LavalinkWsTx, payload); await this._webSocket.SendMessageAsync(payload).ConfigureAwait(false); } internal event NodeDisconnectedEventHandler NodeDisconnected; } }