/// <summary>
        ///     Triggers the <see cref="NodeDisconnected"/> event asynchronously.
        /// </summary>
        /// <param name="eventArgs">the event arguments</param>
        /// <returns>a task that represents the asynchronous operation</returns>
        protected virtual async Task OnNodeDisconnectedAsync(NodeDisconnectedEventArgs eventArgs)
        {
            // stay-online feature
            if (StayOnline && eventArgs.ByRemote)
            {
                _logger?.Log(this, "(Stay-Online) Node died! Moving players to a new node...", LogLevel.Warning);

                var players = eventArgs.Node.GetPlayers <LavalinkPlayer>();

                // move all players
                var tasks = players.Select(player => MovePlayerToNewNodeAsync(eventArgs.Node, player)).ToArray();

                // await until all players were moved to a new node
                await Task.WhenAll(tasks);
            }

            // invoke event
            await NodeDisconnected.InvokeAsync(this, eventArgs);
        }
        /// <summary>
        /// Parse the Svxlink logs
        /// </summary>
        /// <param name="s">one Log line</param>
        public virtual void ParseLog(ChannelBase channel, string s)
        {
            if (string.IsNullOrEmpty(s))
            {
                return;
            }

            if (s.Contains("Connected nodes"))
            {
                Nodes.Clear();
                s.Split(':')[2].Split(',').ToList().ForEach(n => Nodes.Add(new Node {
                    Name = n
                }));
                Connected?.Invoke(channel);
                return;
            }

            if (s.Contains("Node left"))
            {
                var node = new Node {
                    Name = s.Split(":")[2]
                };
                Nodes.Remove(node);
                NodeDisconnected?.Invoke(node);
                return;
            }

            if (s.Contains("Node joined"))
            {
                var node = new Node {
                    Name = s.Split(":")[2]
                };
                Nodes.Add(node);
                NodeConnected?.Invoke(node);
                return;
            }

            if (s.Contains("Talker start"))
            {
                var node = Nodes.Single(nx => nx.Equals(new Node {
                    Name = s.Split(":")[2]
                }));
                node.ClassName = "node node-tx";
                NodeTx?.Invoke(node);
                return;
            }

            if (s.Contains("Talker stop"))
            {
                var node = Nodes.Single(nx => nx.Equals(new Node {
                    Name = s.Split(":")[2]
                }));
                node.ClassName = "node";
                NodeRx?.Invoke(node);
                return;
            }

            if (s.Contains("Access denied"))
            {
                telemetry.TrackTrace($"Impossible de se connecter au salon {channel.Name}. <br/> Accès refusé.", SeverityLevel.Error, channel.TrackProperties);
                Error?.Invoke("Echec de la connexion.", $"Impossible de se connecter au salon {channel.Name}. <br/> Accès refusé.");
                return;
            }

            if (s.Contains("Host not found"))
            {
                telemetry.TrackTrace($"Impossible de se connecter au salon {channel.Name}. <br/> Server introuvable.", SeverityLevel.Error, channel.TrackProperties);
                Error?.Invoke("Echec de la connexion.", $"Impossible de se connecter au salon {channel.Name}. <br/> Server introuvable.");
                return;
            }
        }