using axXez.TS3.Protocol;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace axXez.TS3.Bot
{
/// A music bot instance connected to and playing audio in a specific TeamSpeak channel.
internal sealed class MusicBot : IDisposable
{
private readonly VoiceConnection _connection;
private readonly ConcurrentQueue _queue = new();
/// The underlying voice connection.
public VoiceConnection Connection => _connection;
/// Number of tracks waiting in the queue, not counting the currently playing one.
public int QueueLength => _queue.Count;
/// Raised when a track successfully begins playing.
public event Action TrackStarted;
/// Raised when a track fails to load. The bot automatically advances to the next queued track.
public event Action TrackFailed;
/// Extra arguments forwarded to every yt-dlp invocation (e.g. --cookies).
public IReadOnlyList YtDlpArgs { get; set; } = [];
public MusicBot(VoiceConnection connection)
{
_connection = connection;
_connection.OnConnected += Voice_OnConnected;
_connection.OnDisconnected += Voice_OnDisconnected;
_connection.OnTrackEnded += Voice_OnTrackEnded;
}
/// Starts the voice connection.
public void Start()
=> _connection.Start();
///
/// Adds a URL or file path to the queue. Begins playback immediately if the bot is idle.
/// Returns true if playback started immediately, false if the track was queued.
///
public bool Enqueue(string url)
{
bool wasIdle = !_connection.IsPlaying && !_connection.IsPaused;
_queue.Enqueue(url);
if (wasIdle)
PlayNext();
return wasIdle;
}
/// Adds multiple URLs to the queue. Begins playback immediately if the bot is idle.
public void EnqueueAll(IEnumerable urls)
{
bool wasIdle = !_connection.IsPlaying && !_connection.IsPaused;
foreach (var url in urls)
_queue.Enqueue(url);
if (wasIdle)
PlayNext();
}
/// Stops the current track and immediately advances to the next one in the queue.
public void Skip()
{
_connection.StopPlayback();
PlayNext();
}
/// Stops playback and clears all queued tracks.
public void StopAndClear()
{
while (_queue.TryDequeue(out _)) { }
_connection.StopPlayback();
}
/// Returns a snapshot of the URLs currently waiting in the queue.
public string[] GetQueue()
=> _queue.ToArray();
private void PlayNext()
{
if (!_queue.TryDequeue(out string url))
return;
if (IsYouTubeUrl(url))
PlayYouTubeHandled(url);
else
{
_connection.PlayStream(url);
_connection.SendChannelMessage($"Now playing: [URL]{url}[/URL]");
TrackStarted?.Invoke(url);
}
}
private async void PlayYouTubeHandled(string url)
{
try
{
await _connection.PlayYouTubeAsync(url, YtDlpArgs);
_connection.SendChannelMessage($"Now playing: [URL]{url}[/URL]");
TrackStarted?.Invoke(url);
}
catch (Exception ex)
{
GlobalLogger.LogError("MusicBot", $"Failed to resolve [{url}] for [{_connection.Nickname}]", ex);
_connection.SendChannelMessage($"Failed to play [URL]{url}[/URL]: {ex.Message}");
TrackFailed?.Invoke(url, ex.Message);
PlayNext();
}
}
private static bool IsYouTubeUrl(string url)
=> url.Contains("youtube.com", StringComparison.OrdinalIgnoreCase) ||
url.Contains("youtu.be", StringComparison.OrdinalIgnoreCase);
private void Voice_OnConnected(ITeamspeakClient sender)
{
GlobalLogger.Log("MusicBot", $"[{_connection.Nickname}] connected.");
}
private void Voice_OnDisconnected(ITeamspeakClient sender)
{
GlobalLogger.Log("MusicBot", $"[{_connection.Nickname}] disconnected.", LogType.Warning);
}
private void Voice_OnTrackEnded(VoiceConnection sender)
{
PlayNext();
}
public void Dispose()
{
_connection.OnConnected -= Voice_OnConnected;
_connection.OnDisconnected -= Voice_OnDisconnected;
_connection.OnTrackEnded -= Voice_OnTrackEnded;
while (_queue.TryDequeue(out _)) { }
}
}
}