376 lines
12 KiB
C#
376 lines
12 KiB
C#
using Discord;
|
|
using Discord.Audio;
|
|
using NadekoBot.Extensions;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
namespace NadekoBot.Modules.Music.Classes
|
|
{
|
|
|
|
public enum MusicType
|
|
{
|
|
Radio,
|
|
Normal,
|
|
Local,
|
|
Soundcloud
|
|
}
|
|
|
|
public enum StreamState
|
|
{
|
|
Resolving,
|
|
Queued,
|
|
Playing,
|
|
Completed
|
|
}
|
|
|
|
public class MusicPlayer
|
|
{
|
|
private IAudioClient audioClient { get; set; }
|
|
|
|
/// <summary>
|
|
/// Player will prioritize different queuer name
|
|
/// over the song position in the playlist
|
|
/// </summary>
|
|
public bool FairPlay { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Song will stop playing after this amount of time.
|
|
/// To prevent people queueing radio or looped songs
|
|
/// while other people want to listen to other songs too.
|
|
/// </summary>
|
|
public uint MaxPlaytimeSeconds { get; set; } = 0;
|
|
|
|
|
|
// this should be written better
|
|
public TimeSpan TotalPlaytime =>
|
|
playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ?
|
|
TimeSpan.MaxValue :
|
|
new TimeSpan(playlist.Sum(s => s.TotalTime.Ticks));
|
|
|
|
/// <summary>
|
|
/// Users who recently got their music wish
|
|
/// </summary>
|
|
private ConcurrentHashSet<string> recentlyPlayedUsers { get; } = new ConcurrentHashSet<string>();
|
|
|
|
private readonly List<Song> playlist = new List<Song>();
|
|
public IReadOnlyCollection<Song> Playlist => playlist;
|
|
|
|
public Song CurrentSong { get; private set; }
|
|
public CancellationTokenSource SongCancelSource { get; private set; }
|
|
private CancellationToken cancelToken { get; set; }
|
|
|
|
public bool Paused { get; set; }
|
|
|
|
public float Volume { get; private set; }
|
|
|
|
public event Action<MusicPlayer, Song> OnCompleted = delegate { };
|
|
public event Action<MusicPlayer, Song> OnStarted = delegate { };
|
|
public event Action<bool> OnPauseChanged = delegate { };
|
|
|
|
public IVoiceChannel PlaybackVoiceChannel { get; private set; }
|
|
public ITextChannel OutputTextChannel { get; set; }
|
|
|
|
private bool Destroyed { get; set; } = false;
|
|
public bool RepeatSong { get; private set; } = false;
|
|
public bool RepeatPlaylist { get; private set; } = false;
|
|
public bool Autoplay { get; set; } = false;
|
|
public uint MaxQueueSize { get; set; } = 0;
|
|
|
|
private ConcurrentQueue<Action> actionQueue { get; } = new ConcurrentQueue<Action>();
|
|
|
|
public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%";
|
|
|
|
public event Action<Song, int> SongRemoved = delegate { };
|
|
|
|
public MusicPlayer(IVoiceChannel startingVoiceChannel, ITextChannel outputChannel, float? defaultVolume)
|
|
{
|
|
if (startingVoiceChannel == null)
|
|
throw new ArgumentNullException(nameof(startingVoiceChannel));
|
|
|
|
OutputTextChannel = outputChannel;
|
|
Volume = defaultVolume ?? 1.0f;
|
|
|
|
PlaybackVoiceChannel = startingVoiceChannel;
|
|
SongCancelSource = new CancellationTokenSource();
|
|
cancelToken = SongCancelSource.Token;
|
|
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
while (!Destroyed)
|
|
{
|
|
try
|
|
{
|
|
Action action;
|
|
if (actionQueue.TryDequeue(out action))
|
|
{
|
|
action();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
await Task.Delay(100).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("Action queue crashed");
|
|
Console.WriteLine(ex);
|
|
}
|
|
}).ConfigureAwait(false);
|
|
|
|
var t = new Thread(new ThreadStart(async () =>
|
|
{
|
|
while (!Destroyed)
|
|
{
|
|
try
|
|
{
|
|
if (audioClient != null)
|
|
try { await audioClient.DisconnectAsync().ConfigureAwait(false); } catch { }
|
|
audioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false);
|
|
|
|
CurrentSong = GetNextSong();
|
|
|
|
if (CurrentSong == null)
|
|
continue;
|
|
|
|
var index = playlist.IndexOf(CurrentSong);
|
|
if (index != -1)
|
|
RemoveSongAt(index, true);
|
|
|
|
OnStarted(this, CurrentSong);
|
|
try
|
|
{
|
|
await CurrentSong.Play(audioClient, cancelToken);
|
|
}
|
|
catch(OperationCanceledException)
|
|
{
|
|
OnCompleted(this, CurrentSong);
|
|
}
|
|
|
|
|
|
if (RepeatPlaylist)
|
|
AddSong(CurrentSong, CurrentSong.QueuerName);
|
|
|
|
if (RepeatSong)
|
|
AddSong(CurrentSong, 0);
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("Music thread almost crashed.");
|
|
Console.WriteLine(ex);
|
|
await Task.Delay(3000).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
if (!cancelToken.IsCancellationRequested)
|
|
{
|
|
SongCancelSource.Cancel();
|
|
}
|
|
SongCancelSource = new CancellationTokenSource();
|
|
cancelToken = SongCancelSource.Token;
|
|
CurrentSong = null;
|
|
await Task.Delay(300).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}));
|
|
|
|
t.Start();
|
|
}
|
|
|
|
public void Next()
|
|
{
|
|
actionQueue.Enqueue(() =>
|
|
{
|
|
Paused = false;
|
|
SongCancelSource.Cancel();
|
|
});
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
actionQueue.Enqueue(() =>
|
|
{
|
|
RepeatPlaylist = false;
|
|
RepeatSong = false;
|
|
playlist.Clear();
|
|
if (!SongCancelSource.IsCancellationRequested)
|
|
SongCancelSource.Cancel();
|
|
});
|
|
}
|
|
|
|
public void TogglePause() => OnPauseChanged(Paused = !Paused);
|
|
|
|
public int SetVolume(int volume)
|
|
{
|
|
if (volume < 0)
|
|
volume = 0;
|
|
if (volume > 100)
|
|
volume = 100;
|
|
|
|
Volume = volume / 100.0f;
|
|
return volume;
|
|
}
|
|
|
|
private Song GetNextSong()
|
|
{
|
|
if (!FairPlay)
|
|
{
|
|
return playlist.FirstOrDefault();
|
|
}
|
|
var song = playlist.FirstOrDefault(c => !recentlyPlayedUsers.Contains(c.QueuerName))
|
|
?? playlist.FirstOrDefault();
|
|
|
|
if (song == null)
|
|
return null;
|
|
|
|
if (recentlyPlayedUsers.Contains(song.QueuerName))
|
|
{
|
|
recentlyPlayedUsers.Clear();
|
|
}
|
|
|
|
recentlyPlayedUsers.Add(song.QueuerName);
|
|
return song;
|
|
}
|
|
|
|
public void Shuffle()
|
|
{
|
|
actionQueue.Enqueue(() =>
|
|
{
|
|
var oldPlaylist = playlist.ToArray();
|
|
playlist.Clear();
|
|
playlist.AddRange(oldPlaylist.Shuffle());
|
|
});
|
|
}
|
|
|
|
public void AddSong(Song s, string username)
|
|
{
|
|
if (s == null)
|
|
throw new ArgumentNullException(nameof(s));
|
|
ThrowIfQueueFull();
|
|
actionQueue.Enqueue(() =>
|
|
{
|
|
s.MusicPlayer = this;
|
|
s.QueuerName = username.TrimTo(10);
|
|
playlist.Add(s);
|
|
});
|
|
}
|
|
|
|
public void AddSong(Song s, int index)
|
|
{
|
|
if (s == null)
|
|
throw new ArgumentNullException(nameof(s));
|
|
actionQueue.Enqueue(() =>
|
|
{
|
|
playlist.Insert(index, s);
|
|
});
|
|
}
|
|
|
|
public void RemoveSong(Song s)
|
|
{
|
|
if (s == null)
|
|
throw new ArgumentNullException(nameof(s));
|
|
actionQueue.Enqueue(() =>
|
|
{
|
|
playlist.Remove(s);
|
|
});
|
|
}
|
|
|
|
public void RemoveSongAt(int index, bool silent = false)
|
|
{
|
|
actionQueue.Enqueue(() =>
|
|
{
|
|
if (index < 0 || index >= playlist.Count)
|
|
return;
|
|
var song = playlist.ElementAtOrDefault(index);
|
|
if (playlist.Remove(song) && !silent)
|
|
{
|
|
SongRemoved(song, index);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
public void ClearQueue()
|
|
{
|
|
actionQueue.Enqueue(() =>
|
|
{
|
|
playlist.Clear();
|
|
});
|
|
}
|
|
|
|
public async Task UpdateSongDurationsAsync()
|
|
{
|
|
var curSong = CurrentSong;
|
|
var toUpdate = playlist.Where(s => s.SongInfo.ProviderType == MusicType.Normal &&
|
|
s.TotalTime == TimeSpan.Zero)
|
|
.ToArray();
|
|
if (curSong != null)
|
|
{
|
|
Array.Resize(ref toUpdate, toUpdate.Length + 1);
|
|
toUpdate[toUpdate.Length - 1] = curSong;
|
|
}
|
|
var ids = toUpdate.Select(s => s.SongInfo.Query.Substring(s.SongInfo.Query.LastIndexOf("?v=") + 3))
|
|
.Distinct();
|
|
|
|
var durations = await NadekoBot.Google.GetVideoDurationsAsync(ids);
|
|
|
|
toUpdate.ForEach(s =>
|
|
{
|
|
foreach (var kvp in durations)
|
|
{
|
|
if (s.SongInfo.Query.EndsWith(kvp.Key))
|
|
{
|
|
s.TotalTime = kvp.Value;
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
public void Destroy()
|
|
{
|
|
actionQueue.Enqueue(async () =>
|
|
{
|
|
RepeatPlaylist = false;
|
|
RepeatSong = false;
|
|
Destroyed = true;
|
|
playlist.Clear();
|
|
|
|
try { await audioClient.DisconnectAsync(); } catch { }
|
|
if (!SongCancelSource.IsCancellationRequested)
|
|
SongCancelSource.Cancel();
|
|
});
|
|
}
|
|
|
|
//public async Task MoveToVoiceChannel(IVoiceChannel voiceChannel)
|
|
//{
|
|
// if (audioClient?.ConnectionState != ConnectionState.Connected)
|
|
// throw new InvalidOperationException("Can't move while bot is not connected to voice channel.");
|
|
// PlaybackVoiceChannel = voiceChannel;
|
|
// audioClient = await voiceChannel.ConnectAsync().ConfigureAwait(false);
|
|
//}
|
|
|
|
public bool ToggleRepeatSong() => this.RepeatSong = !this.RepeatSong;
|
|
|
|
public bool ToggleRepeatPlaylist() => this.RepeatPlaylist = !this.RepeatPlaylist;
|
|
|
|
public bool ToggleAutoplay() => this.Autoplay = !this.Autoplay;
|
|
|
|
public void ThrowIfQueueFull()
|
|
{
|
|
if (MaxQueueSize == 0)
|
|
return;
|
|
if (playlist.Count >= MaxQueueSize)
|
|
throw new PlaylistFullException();
|
|
}
|
|
}
|
|
}
|