Removed module projects because it can't work like that atm. Commented out package commands.
This commit is contained in:
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common.Exceptions
|
||||
{
|
||||
public class NotInVoiceChannelException : Exception
|
||||
{
|
||||
public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { }
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common.Exceptions
|
||||
{
|
||||
public class QueueFullException : Exception
|
||||
{
|
||||
public QueueFullException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
public QueueFullException() : base("Queue is full.") { }
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common.Exceptions
|
||||
{
|
||||
public class SongNotFoundException : Exception
|
||||
{
|
||||
public SongNotFoundException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
public SongNotFoundException() : base("Song is not found.") { }
|
||||
}
|
||||
}
|
670
NadekoBot.Core/Modules/Music/Common/MusicPlayer.cs
Normal file
670
NadekoBot.Core/Modules/Music/Common/MusicPlayer.cs
Normal file
@ -0,0 +1,670 @@
|
||||
using Discord;
|
||||
using Discord.Audio;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using System.Linq;
|
||||
using NadekoBot.Extensions;
|
||||
using System.Diagnostics;
|
||||
using NadekoBot.Common.Collections;
|
||||
using NadekoBot.Modules.Music.Services;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common
|
||||
{
|
||||
public enum StreamState
|
||||
{
|
||||
Resolving,
|
||||
Queued,
|
||||
Playing,
|
||||
Completed
|
||||
}
|
||||
public class MusicPlayer
|
||||
{
|
||||
private readonly Thread _player;
|
||||
public IVoiceChannel VoiceChannel { get; private set; }
|
||||
private readonly Logger _log;
|
||||
|
||||
private MusicQueue Queue { get; } = new MusicQueue();
|
||||
|
||||
public bool Exited { get; set; } = false;
|
||||
public bool Stopped { get; private set; } = false;
|
||||
public float Volume { get; private set; } = 1.0f;
|
||||
public bool Paused => pauseTaskSource != null;
|
||||
private TaskCompletionSource<bool> pauseTaskSource { get; set; } = null;
|
||||
|
||||
public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%";
|
||||
public string PrettyCurrentTime
|
||||
{
|
||||
get
|
||||
{
|
||||
var time = CurrentTime.ToString(@"mm\:ss");
|
||||
var hrs = (int)CurrentTime.TotalHours;
|
||||
|
||||
if (hrs > 0)
|
||||
return hrs + ":" + time;
|
||||
else
|
||||
return time;
|
||||
}
|
||||
}
|
||||
public string PrettyFullTime => PrettyCurrentTime + " / " + (Queue.Current.Song?.PrettyTotalTime ?? "?");
|
||||
private CancellationTokenSource SongCancelSource { get; set; }
|
||||
public ITextChannel OutputTextChannel { get; set; }
|
||||
public (int Index, SongInfo Current) Current
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Stopped)
|
||||
return (0, null);
|
||||
return Queue.Current;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RepeatCurrentSong { get; private set; }
|
||||
public bool Shuffle { get; private set; }
|
||||
public bool Autoplay { get; private set; }
|
||||
public bool RepeatPlaylist { get; private set; } = false;
|
||||
public uint MaxQueueSize
|
||||
{
|
||||
get => Queue.MaxQueueSize;
|
||||
set { lock (locker) Queue.MaxQueueSize = value; }
|
||||
}
|
||||
private bool _fairPlay;
|
||||
public bool FairPlay
|
||||
{
|
||||
get => _fairPlay;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
var cur = Queue.Current;
|
||||
if (cur.Song != null)
|
||||
RecentlyPlayedUsers.Add(cur.Song.QueuerName);
|
||||
}
|
||||
else
|
||||
{
|
||||
RecentlyPlayedUsers.Clear();
|
||||
}
|
||||
|
||||
_fairPlay = value;
|
||||
}
|
||||
}
|
||||
public bool AutoDelete { get; set; }
|
||||
public uint MaxPlaytimeSeconds { get; set; }
|
||||
|
||||
|
||||
const int _frameBytes = 3840;
|
||||
const float _miliseconds = 20.0f;
|
||||
public TimeSpan CurrentTime => TimeSpan.FromSeconds(_bytesSent / (float)_frameBytes / (1000 / _miliseconds));
|
||||
|
||||
private int _bytesSent = 0;
|
||||
|
||||
private IAudioClient _audioClient;
|
||||
private readonly object locker = new object();
|
||||
private MusicService _musicService;
|
||||
|
||||
#region events
|
||||
public event Action<MusicPlayer, (int Index, SongInfo Song)> OnStarted;
|
||||
public event Action<MusicPlayer, SongInfo> OnCompleted;
|
||||
public event Action<MusicPlayer, bool> OnPauseChanged;
|
||||
#endregion
|
||||
|
||||
|
||||
private bool manualSkip = false;
|
||||
private bool manualIndex = false;
|
||||
private bool newVoiceChannel = false;
|
||||
private readonly IGoogleApiService _google;
|
||||
|
||||
private bool cancel = false;
|
||||
|
||||
private ConcurrentHashSet<string> RecentlyPlayedUsers { get; } = new ConcurrentHashSet<string>();
|
||||
public TimeSpan TotalPlaytime
|
||||
{
|
||||
get
|
||||
{
|
||||
var songs = Queue.ToArray().Songs;
|
||||
return songs.Any(s => s.TotalTime == TimeSpan.MaxValue)
|
||||
? TimeSpan.MaxValue
|
||||
: new TimeSpan(songs.Sum(s => s.TotalTime.Ticks));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public MusicPlayer(MusicService musicService, IGoogleApiService google, IVoiceChannel vch, ITextChannel output, float volume)
|
||||
{
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
this.Volume = volume;
|
||||
this.VoiceChannel = vch;
|
||||
this.SongCancelSource = new CancellationTokenSource();
|
||||
this.OutputTextChannel = output;
|
||||
this._musicService = musicService;
|
||||
this._google = google;
|
||||
|
||||
_log.Info("Initialized");
|
||||
|
||||
_player = new Thread(new ThreadStart(PlayerLoop));
|
||||
_player.Start();
|
||||
_log.Info("Loop started");
|
||||
}
|
||||
|
||||
private async void PlayerLoop()
|
||||
{
|
||||
while (!Exited)
|
||||
{
|
||||
_bytesSent = 0;
|
||||
cancel = false;
|
||||
CancellationToken cancelToken;
|
||||
(int Index, SongInfo Song) data;
|
||||
lock (locker)
|
||||
{
|
||||
data = Queue.Current;
|
||||
cancelToken = SongCancelSource.Token;
|
||||
manualSkip = false;
|
||||
manualIndex = false;
|
||||
}
|
||||
if (data.Song != null)
|
||||
{
|
||||
_log.Info("Starting");
|
||||
AudioOutStream pcm = null;
|
||||
SongBuffer b = null;
|
||||
try
|
||||
{
|
||||
b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == MusicType.Local);
|
||||
//_log.Info("Created buffer, buffering...");
|
||||
|
||||
//var bufferTask = b.StartBuffering(cancelToken);
|
||||
//var timeout = Task.Delay(10000);
|
||||
//if (Task.WhenAny(bufferTask, timeout) == timeout)
|
||||
//{
|
||||
// _log.Info("Buffering failed due to a timeout.");
|
||||
// continue;
|
||||
//}
|
||||
//else if (!bufferTask.Result)
|
||||
//{
|
||||
// _log.Info("Buffering failed due to a cancel or error.");
|
||||
// continue;
|
||||
//}
|
||||
//_log.Info("Buffered. Getting audio client...");
|
||||
var ac = await GetAudioClient();
|
||||
_log.Info("Got Audio client");
|
||||
if (ac == null)
|
||||
{
|
||||
_log.Info("Can't join");
|
||||
await Task.Delay(900, cancelToken);
|
||||
// just wait some time, maybe bot doesn't even have perms to join that voice channel,
|
||||
// i don't want to spam connection attempts
|
||||
continue;
|
||||
}
|
||||
pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500);
|
||||
_log.Info("Created pcm stream");
|
||||
OnStarted?.Invoke(this, data);
|
||||
|
||||
byte[] buffer = new byte[3840];
|
||||
int bytesRead = 0;
|
||||
|
||||
while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0
|
||||
&& (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds))
|
||||
{
|
||||
AdjustVolume(buffer, Volume);
|
||||
await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
|
||||
unchecked { _bytesSent += bytesRead; }
|
||||
|
||||
await (pauseTaskSource?.Task ?? Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_log.Info("Song Canceled");
|
||||
cancel = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pcm != null)
|
||||
{
|
||||
// flush is known to get stuck from time to time,
|
||||
// just skip flushing if it takes more than 1 second
|
||||
var flushCancel = new CancellationTokenSource();
|
||||
var flushToken = flushCancel.Token;
|
||||
var flushDelay = Task.Delay(1000, flushToken);
|
||||
await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken));
|
||||
flushCancel.Cancel();
|
||||
pcm.Dispose();
|
||||
}
|
||||
|
||||
if (b != null)
|
||||
b.Dispose();
|
||||
|
||||
OnCompleted?.Invoke(this, data.Song);
|
||||
|
||||
if (_bytesSent == 0 && !cancel)
|
||||
{
|
||||
lock (locker)
|
||||
Queue.RemoveSong(data.Song);
|
||||
_log.Info("Song removed because it can't play");
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
//if repeating current song, just ignore other settings,
|
||||
// and play this song again (don't change the index)
|
||||
// ignore rcs if song is manually skipped
|
||||
|
||||
int queueCount;
|
||||
bool stopped;
|
||||
int currentIndex;
|
||||
lock (locker)
|
||||
{
|
||||
queueCount = Queue.Count;
|
||||
stopped = Stopped;
|
||||
currentIndex = Queue.CurrentIndex;
|
||||
}
|
||||
|
||||
if (AutoDelete && !RepeatCurrentSong && !RepeatPlaylist && data.Song != null)
|
||||
{
|
||||
Queue.RemoveSong(data.Song);
|
||||
}
|
||||
|
||||
if (!manualIndex && (!RepeatCurrentSong || manualSkip))
|
||||
{
|
||||
if (Shuffle)
|
||||
{
|
||||
_log.Info("Random song");
|
||||
Queue.Random(); //if shuffle is set, set current song index to a random number
|
||||
}
|
||||
else
|
||||
{
|
||||
//if last song, and autoplay is enabled, and if it's a youtube song
|
||||
// do autplay magix
|
||||
if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == MusicType.YouTube)
|
||||
{
|
||||
try
|
||||
{
|
||||
_log.Info("Loading related song");
|
||||
await _musicService.TryQueueRelatedSongAsync(data.Song, OutputTextChannel, VoiceChannel);
|
||||
if(!AutoDelete)
|
||||
Queue.Next();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_log.Info("Loading related song failed.");
|
||||
}
|
||||
}
|
||||
else if (FairPlay)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
_log.Info("Next fair song");
|
||||
var q = Queue.ToArray().Songs.Shuffle().ToArray();
|
||||
|
||||
bool found = false;
|
||||
for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently
|
||||
{
|
||||
var item = q[i];
|
||||
if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index
|
||||
{
|
||||
Queue.CurrentIndex = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) //if it's not
|
||||
{
|
||||
RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played)
|
||||
Queue.Random(); //go to a random song (to prevent looping on the first few songs)
|
||||
var cur = Current;
|
||||
if (cur.Current != null) // add newely scheduled song's queuer to the recently played list
|
||||
RecentlyPlayedUsers.Add(cur.Current.QueuerName);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip)
|
||||
{
|
||||
_log.Info("Stopping because repeatplaylist is disabled");
|
||||
lock (locker)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.Info("Next song");
|
||||
lock (locker)
|
||||
{
|
||||
if (!Stopped)
|
||||
if(!AutoDelete)
|
||||
Queue.Next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(ex);
|
||||
}
|
||||
}
|
||||
do
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
while ((Queue.Count == 0 || Stopped) && !Exited);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IAudioClient> GetAudioClient(bool reconnect = false)
|
||||
{
|
||||
if (_audioClient == null ||
|
||||
_audioClient.ConnectionState != ConnectionState.Connected ||
|
||||
reconnect ||
|
||||
newVoiceChannel)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var t = _audioClient?.StopAsync();
|
||||
if (t != null)
|
||||
{
|
||||
|
||||
_log.Info("Stopping audio client");
|
||||
await t;
|
||||
|
||||
_log.Info("Disposing audio client");
|
||||
_audioClient.Dispose();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
newVoiceChannel = false;
|
||||
|
||||
_log.Info("Get current user");
|
||||
var curUser = await VoiceChannel.Guild.GetCurrentUserAsync();
|
||||
if (curUser.VoiceChannel != null)
|
||||
{
|
||||
_log.Info("Connecting");
|
||||
var ac = await VoiceChannel.ConnectAsync();
|
||||
_log.Info("Connected, stopping");
|
||||
await ac.StopAsync();
|
||||
_log.Info("Disconnected");
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
_log.Info("Connecting");
|
||||
_audioClient = await VoiceChannel.ConnectAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return _audioClient;
|
||||
}
|
||||
|
||||
public int Enqueue(SongInfo song)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (Exited)
|
||||
return -1;
|
||||
Queue.Add(song);
|
||||
return Queue.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public int EnqueueNext(SongInfo song)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (Exited)
|
||||
return -1;
|
||||
return Queue.AddNext(song);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetIndex(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
lock (locker)
|
||||
{
|
||||
if (Exited)
|
||||
return;
|
||||
if (AutoDelete && index >= Queue.CurrentIndex && index > 0)
|
||||
index--;
|
||||
Queue.CurrentIndex = index;
|
||||
manualIndex = true;
|
||||
Stopped = false;
|
||||
CancelCurrentSong();
|
||||
}
|
||||
}
|
||||
|
||||
public void Next(int skipCount = 1)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (Exited)
|
||||
return;
|
||||
manualSkip = true;
|
||||
// if player is stopped, and user uses .n, it should play current song.
|
||||
// It's a bit weird, but that's the least annoying solution
|
||||
if (!Stopped)
|
||||
if (!RepeatPlaylist && Queue.IsLast()) // if it's the last song in the queue, and repeat playlist is disabled
|
||||
{ //stop the queue
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
else
|
||||
Queue.Next(skipCount - 1);
|
||||
else
|
||||
Queue.CurrentIndex = 0;
|
||||
Stopped = false;
|
||||
CancelCurrentSong();
|
||||
Unpause();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop(bool clearQueue = false)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
Stopped = true;
|
||||
//Queue.ResetCurrent();
|
||||
if (clearQueue)
|
||||
Queue.Clear();
|
||||
Unpause();
|
||||
CancelCurrentSong();
|
||||
}
|
||||
}
|
||||
|
||||
private void Unpause()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (pauseTaskSource != null)
|
||||
{
|
||||
pauseTaskSource.TrySetResult(true);
|
||||
pauseTaskSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TogglePause()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (pauseTaskSource == null)
|
||||
pauseTaskSource = new TaskCompletionSource<bool>();
|
||||
else
|
||||
{
|
||||
Unpause();
|
||||
}
|
||||
}
|
||||
OnPauseChanged?.Invoke(this, pauseTaskSource != null);
|
||||
}
|
||||
|
||||
public void SetVolume(int volume)
|
||||
{
|
||||
if (volume < 0 || volume > 100)
|
||||
throw new ArgumentOutOfRangeException(nameof(volume));
|
||||
lock (locker)
|
||||
{
|
||||
Volume = ((float)volume) / 100;
|
||||
}
|
||||
}
|
||||
|
||||
public SongInfo RemoveAt(int index)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
var cur = Queue.Current;
|
||||
var toReturn = Queue.RemoveAt(index);
|
||||
if (cur.Index == index)
|
||||
Next();
|
||||
return toReturn;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelCurrentSong()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
var cs = SongCancelSource;
|
||||
SongCancelSource = new CancellationTokenSource();
|
||||
cs.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearQueue()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
Queue.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public (int CurrentIndex, SongInfo[] Songs) QueueArray()
|
||||
{
|
||||
lock (locker)
|
||||
return Queue.ToArray();
|
||||
}
|
||||
|
||||
//aidiakapi ftw
|
||||
public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume)
|
||||
{
|
||||
if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples;
|
||||
|
||||
// 16-bit precision for the multiplication
|
||||
var volumeFixed = (int)Math.Round(volume * 65536d);
|
||||
|
||||
var count = audioSamples.Length / 2;
|
||||
|
||||
fixed (byte* srcBytes = audioSamples)
|
||||
{
|
||||
var src = (short*)srcBytes;
|
||||
|
||||
for (var i = count; i != 0; i--, src++)
|
||||
*src = (short)(((*src) * volumeFixed) >> 16);
|
||||
}
|
||||
|
||||
return audioSamples;
|
||||
}
|
||||
|
||||
public bool ToggleRepeatSong()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
return RepeatCurrentSong = !RepeatCurrentSong;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Destroy()
|
||||
{
|
||||
_log.Info("Destroying");
|
||||
lock (locker)
|
||||
{
|
||||
Stop();
|
||||
Exited = true;
|
||||
Unpause();
|
||||
|
||||
OnCompleted = null;
|
||||
OnPauseChanged = null;
|
||||
OnStarted = null;
|
||||
}
|
||||
var ac = _audioClient;
|
||||
if (ac != null)
|
||||
await ac.StopAsync();
|
||||
}
|
||||
|
||||
public bool ToggleShuffle()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
return Shuffle = !Shuffle;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ToggleAutoplay()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
return Autoplay = !Autoplay;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ToggleRepeatPlaylist()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
return RepeatPlaylist = !RepeatPlaylist;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetVoiceChannel(IVoiceChannel vch)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (Exited)
|
||||
return;
|
||||
VoiceChannel = vch;
|
||||
}
|
||||
_audioClient = await vch.ConnectAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateSongDurationsAsync()
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var (_, songs) = Queue.ToArray();
|
||||
var toUpdate = songs
|
||||
.Where(x => x.ProviderType == MusicType.YouTube
|
||||
&& x.TotalTime == TimeSpan.Zero);
|
||||
|
||||
var vIds = toUpdate.Select(x => x.VideoId);
|
||||
|
||||
sw.Stop();
|
||||
_log.Info(sw.Elapsed.TotalSeconds);
|
||||
if (!vIds.Any())
|
||||
return;
|
||||
|
||||
var durations = await _google.GetVideoDurationsAsync(vIds);
|
||||
|
||||
foreach (var x in toUpdate)
|
||||
{
|
||||
if (durations.TryGetValue(x.VideoId, out var dur))
|
||||
x.TotalTime = dur;
|
||||
}
|
||||
}
|
||||
|
||||
public SongInfo MoveSong(int n1, int n2)
|
||||
=> Queue.MoveSong(n1, n2);
|
||||
|
||||
//// 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));
|
||||
}
|
||||
}
|
215
NadekoBot.Core/Modules/Music/Common/MusicQueue.cs
Normal file
215
NadekoBot.Core/Modules/Music/Common/MusicQueue.cs
Normal file
@ -0,0 +1,215 @@
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Music.Common.Exceptions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common
|
||||
{
|
||||
public class MusicQueue : IDisposable
|
||||
{
|
||||
private LinkedList<SongInfo> Songs { get; set; } = new LinkedList<SongInfo>();
|
||||
private int _currentIndex = 0;
|
||||
public int CurrentIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return _currentIndex;
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (Songs.Count == 0)
|
||||
_currentIndex = 0;
|
||||
else
|
||||
_currentIndex = value %= Songs.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
public (int Index, SongInfo Song) Current
|
||||
{
|
||||
get
|
||||
{
|
||||
var cur = CurrentIndex;
|
||||
return (cur, Songs.ElementAtOrDefault(cur));
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object locker = new object();
|
||||
private TaskCompletionSource<bool> nextSource { get; } = new TaskCompletionSource<bool>();
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
return Songs.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private uint _maxQueueSize;
|
||||
public uint MaxQueueSize
|
||||
{
|
||||
get => _maxQueueSize;
|
||||
set
|
||||
{
|
||||
if (value < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
lock (locker)
|
||||
{
|
||||
_maxQueueSize = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(SongInfo song)
|
||||
{
|
||||
song.ThrowIfNull(nameof(song));
|
||||
lock (locker)
|
||||
{
|
||||
if(MaxQueueSize != 0 && Songs.Count >= MaxQueueSize)
|
||||
throw new QueueFullException();
|
||||
Songs.AddLast(song);
|
||||
}
|
||||
}
|
||||
|
||||
public int AddNext(SongInfo song)
|
||||
{
|
||||
song.ThrowIfNull(nameof(song));
|
||||
lock (locker)
|
||||
{
|
||||
if (MaxQueueSize != 0 && Songs.Count >= MaxQueueSize)
|
||||
throw new QueueFullException();
|
||||
var curSong = Current.Song;
|
||||
if (curSong == null)
|
||||
{
|
||||
Songs.AddLast(song);
|
||||
return Songs.Count;
|
||||
}
|
||||
|
||||
var songlist = Songs.ToList();
|
||||
songlist.Insert(CurrentIndex + 1, song);
|
||||
Songs = new LinkedList<SongInfo>(songlist);
|
||||
return CurrentIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void Next(int skipCount = 1)
|
||||
{
|
||||
lock(locker)
|
||||
CurrentIndex += skipCount;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
public SongInfo RemoveAt(int index)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (index < 0 || index >= Songs.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
var current = Songs.First.Value;
|
||||
for (int i = 0; i < Songs.Count; i++)
|
||||
{
|
||||
if (i == index)
|
||||
{
|
||||
current = Songs.ElementAt(index);
|
||||
Songs.Remove(current);
|
||||
if (CurrentIndex != 0)
|
||||
{
|
||||
if (CurrentIndex >= index)
|
||||
{
|
||||
--CurrentIndex;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
Songs.Clear();
|
||||
CurrentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public (int CurrentIndex, SongInfo[] Songs) ToArray()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
return (CurrentIndex, Songs.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetCurrent()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
CurrentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Random()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
CurrentIndex = new NadekoRandom().Next(Songs.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public SongInfo MoveSong(int n1, int n2)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
var currentSong = Current.Song;
|
||||
var playlist = Songs.ToList();
|
||||
if (n1 >= playlist.Count || n2 >= playlist.Count || n1 == n2)
|
||||
return null;
|
||||
|
||||
var s = playlist[n1];
|
||||
|
||||
playlist.RemoveAt(n1);
|
||||
playlist.Insert(n2, s);
|
||||
|
||||
Songs = new LinkedList<SongInfo>(playlist);
|
||||
|
||||
|
||||
if (currentSong != null)
|
||||
CurrentIndex = playlist.IndexOf(currentSong);
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveSong(SongInfo song)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
Songs.Remove(song);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLast()
|
||||
{
|
||||
lock (locker)
|
||||
return CurrentIndex == Songs.Count - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
//O O [O] O O O O
|
||||
//
|
||||
// 3
|
95
NadekoBot.Core/Modules/Music/Common/SongBuffer.cs
Normal file
95
NadekoBot.Core/Modules/Music/Common/SongBuffer.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common
|
||||
{
|
||||
public class SongBuffer : IDisposable
|
||||
{
|
||||
const int readSize = 81920;
|
||||
private Process p;
|
||||
private Stream _outStream;
|
||||
|
||||
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
|
||||
private readonly Logger _log;
|
||||
|
||||
public string SongUri { get; private set; }
|
||||
|
||||
public SongBuffer(string songUri, string skipTo, bool isLocal)
|
||||
{
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
this.SongUri = songUri;
|
||||
this._isLocal = isLocal;
|
||||
|
||||
try
|
||||
{
|
||||
this.p = StartFFmpegProcess(SongUri, 0);
|
||||
this._outStream = this.p.StandardOutput.BaseStream;
|
||||
}
|
||||
catch (System.ComponentModel.Win32Exception)
|
||||
{
|
||||
_log.Error(@"You have not properly installed or configured FFMPEG.
|
||||
Please install and configure FFMPEG to play music.
|
||||
Check the guides for your platform on how to setup ffmpeg correctly:
|
||||
Windows Guide: https://goo.gl/OjKk8F
|
||||
Linux Guide: https://goo.gl/ShjCUo");
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (InvalidOperationException) { } // when ffmpeg is disposed
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Info(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Process StartFFmpegProcess(string songUri, float skipTo = 0)
|
||||
{
|
||||
var args = $"-err_detect ignore_err -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error";
|
||||
if (!_isLocal)
|
||||
args = "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 " + args;
|
||||
|
||||
return Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "ffmpeg",
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = false,
|
||||
CreateNoWindow = true,
|
||||
});
|
||||
}
|
||||
|
||||
private readonly object locker = new object();
|
||||
private readonly bool _isLocal;
|
||||
|
||||
public int Read(byte[] b, int offset, int toRead)
|
||||
{
|
||||
lock (locker)
|
||||
return _outStream.Read(b, offset, toRead);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.p.StandardOutput.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(ex);
|
||||
}
|
||||
try
|
||||
{
|
||||
if(!this.p.HasExited)
|
||||
this.p.Kill();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
_outStream.Dispose();
|
||||
this.p.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
9
NadekoBot.Core/Modules/Music/Common/SongHandler.cs
Normal file
9
NadekoBot.Core/Modules/Music/Common/SongHandler.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using NLog;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common
|
||||
{
|
||||
public static class SongHandler
|
||||
{
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
}
|
||||
}
|
79
NadekoBot.Core/Modules/Music/Common/SongInfo.cs
Normal file
79
NadekoBot.Core/Modules/Music/Common/SongInfo.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using Discord;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common
|
||||
{
|
||||
public class SongInfo
|
||||
{
|
||||
public string Provider { get; set; }
|
||||
public MusicType ProviderType { get; set; }
|
||||
public string Query { get; set; }
|
||||
public string Title { get; set; }
|
||||
public Func<Task<string>> Uri { get; set; }
|
||||
public string Thumbnail { get; set; }
|
||||
public string QueuerName { get; set; }
|
||||
public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
|
||||
|
||||
public string PrettyProvider => (Provider ?? "???");
|
||||
//public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime;
|
||||
public string PrettyName => $"**[{Title.TrimTo(65)}]({SongUrl})**";
|
||||
public string PrettyInfo => $"{PrettyTotalTime} | {PrettyProvider} | {QueuerName}";
|
||||
public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {Format.Sanitize(QueuerName.TrimTo(15))}`";
|
||||
public string PrettyTotalTime
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TotalTime == TimeSpan.Zero)
|
||||
return "(?)";
|
||||
if (TotalTime == TimeSpan.MaxValue)
|
||||
return "∞";
|
||||
var time = TotalTime.ToString(@"mm\:ss");
|
||||
var hrs = (int)TotalTime.TotalHours;
|
||||
|
||||
if (hrs > 0)
|
||||
return hrs + ":" + time;
|
||||
return time;
|
||||
}
|
||||
}
|
||||
|
||||
public string SongUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (ProviderType)
|
||||
{
|
||||
case MusicType.YouTube:
|
||||
return Query;
|
||||
case MusicType.Soundcloud:
|
||||
return Query;
|
||||
case MusicType.Local:
|
||||
return $"https://google.com/search?q={ WebUtility.UrlEncode(Title).Replace(' ', '+') }";
|
||||
case MusicType.Radio:
|
||||
return $"https://google.com/search?q={Title}";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
private string _videoId = null;
|
||||
public string VideoId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ProviderType == MusicType.YouTube)
|
||||
return _videoId = _videoId ?? videoIdRegex.Match(Query)?.ToString();
|
||||
|
||||
return _videoId ?? "";
|
||||
}
|
||||
|
||||
set => _videoId = value;
|
||||
}
|
||||
|
||||
private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using NadekoBot.Modules.Music.Common.SongResolver.Strategies;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common.SongResolver
|
||||
{
|
||||
public interface ISongResolverFactory
|
||||
{
|
||||
Task<IResolveStrategy> GetResolveStrategy(string query, MusicType? musicType);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using NadekoBot.Modules.Music.Common.SongResolver.Strategies;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common.SongResolver
|
||||
{
|
||||
public class SongResolverFactory : ISongResolverFactory
|
||||
{
|
||||
private readonly SoundCloudApiService _sc;
|
||||
|
||||
public SongResolverFactory(SoundCloudApiService sc)
|
||||
{
|
||||
_sc = sc;
|
||||
}
|
||||
|
||||
public async Task<IResolveStrategy> GetResolveStrategy(string query, MusicType? musicType)
|
||||
{
|
||||
await Task.Yield(); //for async warning
|
||||
switch (musicType)
|
||||
{
|
||||
case MusicType.YouTube:
|
||||
return new YoutubeResolveStrategy();
|
||||
case MusicType.Radio:
|
||||
return new RadioResolveStrategy();
|
||||
case MusicType.Local:
|
||||
return new LocalSongResolveStrategy();
|
||||
case MusicType.Soundcloud:
|
||||
return new SoundcloudResolveStrategy(_sc);
|
||||
default:
|
||||
if (_sc.IsSoundCloudLink(query))
|
||||
return new SoundcloudResolveStrategy(_sc);
|
||||
else if (RadioResolveStrategy.IsRadioLink(query))
|
||||
return new RadioResolveStrategy();
|
||||
// maybe add a check for local files in the future
|
||||
else
|
||||
return new YoutubeResolveStrategy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
|
||||
{
|
||||
public interface IResolveStrategy
|
||||
{
|
||||
Task<SongInfo> ResolveSong(string query);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
|
||||
{
|
||||
public class LocalSongResolveStrategy : IResolveStrategy
|
||||
{
|
||||
public Task<SongInfo> ResolveSong(string query)
|
||||
{
|
||||
return Task.FromResult(new SongInfo
|
||||
{
|
||||
Uri = () => Task.FromResult("\"" + Path.GetFullPath(query) + "\""),
|
||||
Title = Path.GetFileNameWithoutExtension(query),
|
||||
Provider = "Local File",
|
||||
ProviderType = MusicType.Local,
|
||||
Query = query,
|
||||
Thumbnail = "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
|
||||
{
|
||||
public class RadioResolveStrategy : IResolveStrategy
|
||||
{
|
||||
private readonly Regex plsRegex = new Regex("File1=(?<url>.*?)\\n", RegexOptions.Compiled);
|
||||
private readonly Regex m3uRegex = new Regex("(?<url>^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
private readonly Regex asxRegex = new Regex("<ref href=\"(?<url>.*?)\"", RegexOptions.Compiled);
|
||||
private readonly Regex xspfRegex = new Regex("<location>(?<url>.*?)</location>", RegexOptions.Compiled);
|
||||
private readonly Logger _log;
|
||||
|
||||
public RadioResolveStrategy()
|
||||
{
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
}
|
||||
|
||||
public async Task<SongInfo> ResolveSong(string query)
|
||||
{
|
||||
if (IsRadioLink(query))
|
||||
query = await HandleStreamContainers(query);
|
||||
|
||||
return new SongInfo
|
||||
{
|
||||
Uri = () => Task.FromResult(query),
|
||||
Title = query,
|
||||
Provider = "Radio Stream",
|
||||
ProviderType = MusicType.Radio,
|
||||
Query = query,
|
||||
TotalTime = TimeSpan.MaxValue,
|
||||
Thumbnail = "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png",
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsRadioLink(string query) =>
|
||||
(query.StartsWith("http") ||
|
||||
query.StartsWith("ww"))
|
||||
&&
|
||||
(query.Contains(".pls") ||
|
||||
query.Contains(".m3u") ||
|
||||
query.Contains(".asx") ||
|
||||
query.Contains(".xspf"));
|
||||
|
||||
private async Task<string> HandleStreamContainers(string query)
|
||||
{
|
||||
string file = null;
|
||||
try
|
||||
{
|
||||
using (var http = new HttpClient())
|
||||
{
|
||||
file = await http.GetStringAsync(query).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return query;
|
||||
}
|
||||
if (query.Contains(".pls"))
|
||||
{
|
||||
//File1=http://armitunes.com:8000/
|
||||
//Regex.Match(query)
|
||||
try
|
||||
{
|
||||
var m = plsRegex.Match(file);
|
||||
var res = m.Groups["url"]?.ToString();
|
||||
return res?.Trim();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_log.Warn($"Failed reading .pls:\n{file}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (query.Contains(".m3u"))
|
||||
{
|
||||
/*
|
||||
# This is a comment
|
||||
C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
|
||||
C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
|
||||
*/
|
||||
try
|
||||
{
|
||||
var m = m3uRegex.Match(file);
|
||||
var res = m.Groups["url"]?.ToString();
|
||||
return res?.Trim();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_log.Warn($"Failed reading .m3u:\n{file}");
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
if (query.Contains(".asx"))
|
||||
{
|
||||
//<ref href="http://armitunes.com:8000"/>
|
||||
try
|
||||
{
|
||||
var m = asxRegex.Match(file);
|
||||
var res = m.Groups["url"]?.ToString();
|
||||
return res?.Trim();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_log.Warn($"Failed reading .asx:\n{file}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (query.Contains(".xspf"))
|
||||
{
|
||||
/*
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<playlist version="1" xmlns="http://xspf.org/ns/0/">
|
||||
<trackList>
|
||||
<track><location>file:///mp3s/song_1.mp3</location></track>
|
||||
*/
|
||||
try
|
||||
{
|
||||
var m = xspfRegex.Match(file);
|
||||
var res = m.Groups["url"]?.ToString();
|
||||
return res?.Trim();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_log.Warn($"Failed reading .xspf:\n{file}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
using NadekoBot.Modules.Music.Extensions;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
|
||||
{
|
||||
public class SoundcloudResolveStrategy : IResolveStrategy
|
||||
{
|
||||
private readonly SoundCloudApiService _sc;
|
||||
|
||||
public SoundcloudResolveStrategy(SoundCloudApiService sc)
|
||||
{
|
||||
_sc = sc;
|
||||
}
|
||||
|
||||
public async Task<SongInfo> ResolveSong(string query)
|
||||
{
|
||||
var svideo = !_sc.IsSoundCloudLink(query) ?
|
||||
await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false) :
|
||||
await _sc.ResolveVideoAsync(query).ConfigureAwait(false);
|
||||
|
||||
if (svideo == null)
|
||||
return null;
|
||||
return await svideo.GetSongInfo();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
|
||||
{
|
||||
public class YoutubeResolveStrategy : IResolveStrategy
|
||||
{
|
||||
private readonly Logger _log;
|
||||
|
||||
public YoutubeResolveStrategy()
|
||||
{
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
}
|
||||
|
||||
public async Task<SongInfo> ResolveSong(string query)
|
||||
{
|
||||
_log.Info("Getting link");
|
||||
string[] data;
|
||||
try
|
||||
{
|
||||
using (var ytdl = new YtdlOperation())
|
||||
{
|
||||
data = (await ytdl.GetDataAsync(query)).Split('\n');
|
||||
}
|
||||
if (data.Length < 6)
|
||||
{
|
||||
_log.Info("No song found. Data less than 6");
|
||||
return null;
|
||||
}
|
||||
TimeSpan time;
|
||||
if (!TimeSpan.TryParseExact(data[4], new[] { "ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss" }, CultureInfo.InvariantCulture, out time))
|
||||
time = TimeSpan.FromHours(24);
|
||||
|
||||
return new SongInfo()
|
||||
{
|
||||
Title = data[0],
|
||||
VideoId = data[1],
|
||||
Uri = async () =>
|
||||
{
|
||||
using (var ytdl = new YtdlOperation())
|
||||
{
|
||||
data = (await ytdl.GetDataAsync(query)).Split('\n');
|
||||
}
|
||||
if (data.Length < 6)
|
||||
{
|
||||
_log.Info("No song found. Data less than 6");
|
||||
return null;
|
||||
}
|
||||
return data[2];
|
||||
},
|
||||
Thumbnail = data[3],
|
||||
TotalTime = time,
|
||||
Provider = "YouTube",
|
||||
ProviderType = MusicType.YouTube,
|
||||
Query = "https://youtube.com/watch?v=" + data[1],
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
NadekoBot.Core/Modules/Music/Extensions/Extensions.cs
Normal file
23
NadekoBot.Core/Modules/Music/Extensions/Extensions.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using NadekoBot.Modules.Music.Common;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Extensions
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static Task<SongInfo> GetSongInfo(this SoundCloudVideo svideo) =>
|
||||
Task.FromResult(new SongInfo
|
||||
{
|
||||
Title = svideo.FullName,
|
||||
Provider = "SoundCloud",
|
||||
Uri = () => svideo.StreamLink(),
|
||||
ProviderType = MusicType.Soundcloud,
|
||||
Query = svideo.TrackLink,
|
||||
Thumbnail = svideo.artwork_url,
|
||||
TotalTime = TimeSpan.FromMilliseconds(svideo.Duration)
|
||||
});
|
||||
}
|
||||
}
|
901
NadekoBot.Core/Modules/Music/Music.cs
Normal file
901
NadekoBot.Core/Modules/Music/Music.cs
Normal file
@ -0,0 +1,901 @@
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Core.Services;
|
||||
using Discord;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NadekoBot.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Common.Collections;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using NadekoBot.Modules.Music.Services;
|
||||
using NadekoBot.Modules.Music.Common.Exceptions;
|
||||
using NadekoBot.Modules.Music.Common;
|
||||
using NadekoBot.Modules.Music.Extensions;
|
||||
|
||||
namespace NadekoBot.Modules.Music
|
||||
{
|
||||
[NoPublicBot]
|
||||
public class Music : NadekoTopLevelModule<MusicService>
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IGoogleApiService _google;
|
||||
private readonly DbService _db;
|
||||
|
||||
public Music(DiscordSocketClient client,
|
||||
IBotCredentials creds,
|
||||
IGoogleApiService google,
|
||||
DbService db)
|
||||
{
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
_google = google;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
//todo 50 changing server region is bugged again
|
||||
//private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState)
|
||||
//{
|
||||
// var t = Task.Run(() =>
|
||||
// {
|
||||
// var usr = iusr as SocketGuildUser;
|
||||
// if (usr == null ||
|
||||
// oldState.VoiceChannel == newState.VoiceChannel)
|
||||
// return;
|
||||
|
||||
// var player = _music.GetPlayerOrDefault(usr.Guild.Id);
|
||||
|
||||
// if (player == null)
|
||||
// return;
|
||||
|
||||
// try
|
||||
// {
|
||||
// //if bot moved
|
||||
// if ((player.VoiceChannel == oldState.VoiceChannel) &&
|
||||
// usr.Id == _client.CurrentUser.Id)
|
||||
// {
|
||||
// //if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel
|
||||
// // player.TogglePause();
|
||||
// //else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel
|
||||
// // player.TogglePause();
|
||||
|
||||
// // player.SetVoiceChannel(newState.VoiceChannel);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// ////if some other user moved
|
||||
// //if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause
|
||||
// // player.Paused &&
|
||||
// // newState.VoiceChannel.Users.Count >= 2) || // keep in mind bot is in the channel (+1)
|
||||
// // (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause
|
||||
// // !player.Paused &&
|
||||
// // oldState.VoiceChannel.Users.Count == 1))
|
||||
// //{
|
||||
// // player.TogglePause();
|
||||
// // return;
|
||||
// //}
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
// // ignored
|
||||
// }
|
||||
// });
|
||||
// return Task.CompletedTask;
|
||||
//}
|
||||
|
||||
private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent, bool queueFirst = false)
|
||||
{
|
||||
if (songInfo == null)
|
||||
{
|
||||
if(!silent)
|
||||
await ReplyErrorLocalized("song_not_found").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
int index;
|
||||
try
|
||||
{
|
||||
index = queueFirst
|
||||
? mp.EnqueueNext(songInfo)
|
||||
: mp.Enqueue(songInfo);
|
||||
}
|
||||
catch (QueueFullException)
|
||||
{
|
||||
await ReplyErrorLocalized("queue_full", mp.MaxQueueSize).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
if (index != -1)
|
||||
{
|
||||
if (!silent)
|
||||
{
|
||||
try
|
||||
{
|
||||
var embed = new EmbedBuilder().WithOkColor()
|
||||
.WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index + 1)).WithMusicIcon())
|
||||
.WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ")
|
||||
.WithFooter(ef => ef.WithText(songInfo.PrettyProvider));
|
||||
|
||||
if (Uri.IsWellFormedUriString(songInfo.Thumbnail, UriKind.Absolute))
|
||||
embed.WithThumbnailUrl(songInfo.Thumbnail);
|
||||
|
||||
var queuedMessage = await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
if (mp.Stopped)
|
||||
{
|
||||
(await ReplyErrorLocalized("queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10);
|
||||
}
|
||||
queuedMessage?.DeleteAfter(10);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Play([Remainder] string query = null)
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
await Next();
|
||||
}
|
||||
else if (int.TryParse(query, out var index))
|
||||
if (index >= 1)
|
||||
mp.SetIndex(index - 1);
|
||||
else
|
||||
return;
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
await Queue(query);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Queue([Remainder] string query)
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var songInfo = await _service.ResolveSong(query, Context.User.ToString());
|
||||
try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; }
|
||||
if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages)
|
||||
{
|
||||
Context.Message.DeleteAfter(10);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task QueueNext([Remainder] string query)
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var songInfo = await _service.ResolveSong(query, Context.User.ToString());
|
||||
try { await InternalQueue(mp, songInfo, false, true); } catch (QueueFullException) { return; }
|
||||
if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages)
|
||||
{
|
||||
Context.Message.DeleteAfter(10);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task QueueSearch([Remainder] string query)
|
||||
{
|
||||
var videos = (await _google.GetVideoInfosByKeywordAsync(query, 5))
|
||||
.ToArray();
|
||||
|
||||
if (!videos.Any())
|
||||
{
|
||||
await ReplyErrorLocalized("song_not_found").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = await Context.Channel.SendConfirmAsync(string.Join("\n", videos.Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Name)}\n\t{x.Url}")));
|
||||
|
||||
try
|
||||
{
|
||||
var input = await GetUserInputAsync(Context.User.Id, Context.Channel.Id);
|
||||
if (input == null
|
||||
|| !int.TryParse(input, out var index)
|
||||
|| (index -= 1) < 0
|
||||
|| index >= videos.Length)
|
||||
{
|
||||
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
|
||||
return;
|
||||
}
|
||||
|
||||
query = videos[index].Url;
|
||||
|
||||
await Queue(query).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task ListQueue(int page = 0)
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var (current, songs) = mp.QueueArray();
|
||||
|
||||
if (!songs.Any())
|
||||
{
|
||||
await ReplyErrorLocalized("no_player").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (--page < -1)
|
||||
return;
|
||||
|
||||
try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { }
|
||||
|
||||
const int itemsPerPage = 10;
|
||||
|
||||
if (page == -1)
|
||||
page = current / itemsPerPage;
|
||||
|
||||
//if page is 0 (-1 after this decrement) that means default to the page current song is playing from
|
||||
var total = mp.TotalPlaytime;
|
||||
var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format",
|
||||
(int)total.TotalHours,
|
||||
total.Minutes,
|
||||
total.Seconds);
|
||||
var maxPlaytime = mp.MaxPlaytimeSeconds;
|
||||
var lastPage = songs.Length / itemsPerPage;
|
||||
Func<int, EmbedBuilder> printAction = curPage =>
|
||||
{
|
||||
var startAt = itemsPerPage * curPage;
|
||||
var number = 0 + startAt;
|
||||
var desc = string.Join("\n", songs
|
||||
.Skip(startAt)
|
||||
.Take(itemsPerPage)
|
||||
.Select(v =>
|
||||
{
|
||||
if(number++ == current)
|
||||
return $"**⇒**`{number}.` {v.PrettyFullName}";
|
||||
else
|
||||
return $"`{number}.` {v.PrettyFullName}";
|
||||
}));
|
||||
|
||||
desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc;
|
||||
|
||||
var add = "";
|
||||
if (mp.Stopped)
|
||||
add += Format.Bold(GetText("queue_stopped", Format.Code(Prefix + "play"))) + "\n";
|
||||
var mps = mp.MaxPlaytimeSeconds;
|
||||
if (mps > 0)
|
||||
add += Format.Bold(GetText("song_skips_after", TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss"))) + "\n";
|
||||
if (mp.RepeatCurrentSong)
|
||||
add += "🔂 " + GetText("repeating_cur_song") + "\n";
|
||||
else if (mp.Shuffle)
|
||||
add += "🔀 " + GetText("shuffling_playlist") + "\n";
|
||||
else
|
||||
{
|
||||
if (mp.Autoplay)
|
||||
add += "↪ " + GetText("autoplaying") + "\n";
|
||||
if (mp.FairPlay && !mp.Autoplay)
|
||||
add += " " + GetText("fairplay") + "\n";
|
||||
else if (mp.RepeatPlaylist)
|
||||
add += "🔁 " + GetText("repeating_playlist") + "\n";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(add))
|
||||
desc = add + "\n" + desc;
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1))
|
||||
.WithMusicIcon())
|
||||
.WithDescription(desc)
|
||||
.WithFooter(ef => ef.WithText($"{mp.PrettyVolume} | {songs.Length} " +
|
||||
$"{("tracks".SnPl(songs.Length))} | {totalStr}"))
|
||||
.WithOkColor();
|
||||
|
||||
return embed;
|
||||
};
|
||||
await Context.Channel.SendPaginatedConfirmAsync(_client, page, printAction, lastPage, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Next(int skipCount = 1)
|
||||
{
|
||||
if (skipCount < 1)
|
||||
return;
|
||||
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
|
||||
mp.Next(skipCount);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Stop()
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
mp.Stop();
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Destroy()
|
||||
{
|
||||
await _service.DestroyPlayer(Context.Guild.Id);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Pause()
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
mp.TogglePause();
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Volume(int val)
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
if (val < 0 || val > 100)
|
||||
{
|
||||
await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
mp.SetVolume(val);
|
||||
await ReplyConfirmLocalized("volume_set", val).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Defvol([Remainder] int val)
|
||||
{
|
||||
if (val < 0 || val > 100)
|
||||
{
|
||||
await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
uow.GuildConfigs.For(Context.Guild.Id, set => set).DefaultMusicVolume = val / 100.0f;
|
||||
uow.Complete();
|
||||
}
|
||||
await ReplyConfirmLocalized("defvol_set", val).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(1)]
|
||||
public async Task SongRemove(int index)
|
||||
{
|
||||
if (index < 1)
|
||||
{
|
||||
await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
try
|
||||
{
|
||||
var song = mp.RemoveAt(index - 1);
|
||||
var embed = new EmbedBuilder()
|
||||
.WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index)).WithMusicIcon())
|
||||
.WithDescription(song.PrettyName)
|
||||
.WithFooter(ef => ef.WithText(song.PrettyInfo))
|
||||
.WithErrorColor();
|
||||
|
||||
await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public enum All { All }
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(0)]
|
||||
public async Task SongRemove(All all)
|
||||
{
|
||||
var mp = _service.GetPlayerOrDefault(Context.Guild.Id);
|
||||
if (mp == null)
|
||||
return;
|
||||
mp.Stop(true);
|
||||
await ReplyConfirmLocalized("queue_cleared").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Playlists([Remainder] int num = 1)
|
||||
{
|
||||
if (num <= 0)
|
||||
return;
|
||||
|
||||
List<MusicPlaylist> playlists;
|
||||
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithAuthor(eab => eab.WithName(GetText("playlists_page", num)).WithMusicIcon())
|
||||
.WithDescription(string.Join("\n", playlists.Select(r =>
|
||||
GetText("playlists", r.Id, r.Name, r.Author, r.Songs.Count))))
|
||||
.WithOkColor();
|
||||
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task DeletePlaylist([Remainder] int id)
|
||||
{
|
||||
var success = false;
|
||||
try
|
||||
{
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
var pl = uow.MusicPlaylists.Get(id);
|
||||
|
||||
if (pl != null)
|
||||
{
|
||||
if (_creds.IsOwner(Context.User) || pl.AuthorId == Context.User.Id)
|
||||
{
|
||||
uow.MusicPlaylists.Remove(pl);
|
||||
await uow.CompleteAsync().ConfigureAwait(false);
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!success)
|
||||
await ReplyErrorLocalized("playlist_delete_fail").ConfigureAwait(false);
|
||||
else
|
||||
await ReplyConfirmLocalized("playlist_deleted").ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Save([Remainder] string name)
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
|
||||
var songs = mp.QueueArray().Songs
|
||||
.Select(s => new PlaylistSong()
|
||||
{
|
||||
Provider = s.Provider,
|
||||
ProviderType = s.ProviderType,
|
||||
Title = s.Title,
|
||||
Query = s.Query,
|
||||
}).ToList();
|
||||
|
||||
MusicPlaylist playlist;
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
playlist = new MusicPlaylist
|
||||
{
|
||||
Name = name,
|
||||
Author = Context.User.Username,
|
||||
AuthorId = Context.User.Id,
|
||||
Songs = songs.ToList(),
|
||||
};
|
||||
uow.MusicPlaylists.Add(playlist);
|
||||
await uow.CompleteAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("playlist_saved"))
|
||||
.AddField(efb => efb.WithName(GetText("name")).WithValue(name))
|
||||
.AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString())));
|
||||
}
|
||||
|
||||
private static readonly ConcurrentHashSet<ulong> PlaylistLoadBlacklist = new ConcurrentHashSet<ulong>();
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Load([Remainder] int id)
|
||||
{
|
||||
if (!PlaylistLoadBlacklist.Add(Context.Guild.Id))
|
||||
return;
|
||||
try
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
MusicPlaylist mpl;
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
mpl = uow.MusicPlaylists.GetWithSongs(id);
|
||||
}
|
||||
|
||||
if (mpl == null)
|
||||
{
|
||||
await ReplyErrorLocalized("playlist_id_not_found").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
IUserMessage msg = null;
|
||||
try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString()))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); }
|
||||
foreach (var item in mpl.Songs)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _service.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false);
|
||||
}
|
||||
catch (SongNotFoundException) { }
|
||||
catch { break; }
|
||||
}
|
||||
if (msg != null)
|
||||
await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PlaylistLoadBlacklist.TryRemove(Context.Guild.Id);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Fairplay()
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var val = mp.FairPlay = !mp.FairPlay;
|
||||
|
||||
if (val)
|
||||
{
|
||||
await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task SongAutoDelete()
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var val = mp.AutoDelete = !mp.AutoDelete;
|
||||
|
||||
if (val)
|
||||
{
|
||||
await ReplyConfirmLocalized("sad_enabled").ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalized("sad_disabled").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task SoundCloudQueue([Remainder] string query)
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var song = await _service.ResolveSong(query, Context.User.ToString(), MusicType.Soundcloud);
|
||||
await InternalQueue(mp, song, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task SoundCloudPl([Remainder] string pl)
|
||||
{
|
||||
pl = pl?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pl))
|
||||
return;
|
||||
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
|
||||
using (var http = new HttpClient())
|
||||
{
|
||||
var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject<SoundCloudVideo[]>();
|
||||
IUserMessage msg = null;
|
||||
try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(scvids.Length.ToString()))).ConfigureAwait(false); } catch { }
|
||||
foreach (var svideo in scvids)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Yield();
|
||||
var sinfo = await svideo.GetSongInfo();
|
||||
sinfo.QueuerName = Context.User.ToString();
|
||||
await InternalQueue(mp, sinfo, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (msg != null)
|
||||
await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task NowPlaying()
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var (_, currentSong) = mp.Current;
|
||||
if (currentSong == null)
|
||||
return;
|
||||
try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { }
|
||||
|
||||
var embed = new EmbedBuilder().WithOkColor()
|
||||
.WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon())
|
||||
.WithDescription(currentSong.PrettyName)
|
||||
.WithThumbnailUrl(currentSong.Thumbnail)
|
||||
.WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + mp.PrettyFullTime + $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}"));
|
||||
|
||||
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task ShufflePlaylist()
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var val = mp.ToggleShuffle();
|
||||
if(val)
|
||||
await ReplyConfirmLocalized("songs_shuffle_enable").ConfigureAwait(false);
|
||||
else
|
||||
await ReplyConfirmLocalized("songs_shuffle_disable").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Playlist([Remainder] string playlist)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(playlist))
|
||||
return;
|
||||
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
|
||||
var plId = (await _google.GetPlaylistIdsByKeywordsAsync(playlist).ConfigureAwait(false)).FirstOrDefault();
|
||||
if (plId == null)
|
||||
{
|
||||
await ReplyErrorLocalized("no_search_results").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false);
|
||||
if (!ids.Any())
|
||||
{
|
||||
await ReplyErrorLocalized("no_search_results").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var count = ids.Count();
|
||||
var msg = await Context.Channel.SendMessageAsync("🎵 " + GetText("attempting_to_queue",
|
||||
Format.Bold(count.ToString()))).ConfigureAwait(false);
|
||||
|
||||
foreach (var song in ids)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (mp.Exited)
|
||||
return;
|
||||
|
||||
await Task.WhenAll(Task.Delay(150), InternalQueue(mp, await _service.ResolveSong(song, Context.User.ToString(), MusicType.YouTube), true));
|
||||
}
|
||||
catch (SongNotFoundException) { }
|
||||
catch { break; }
|
||||
}
|
||||
|
||||
await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Radio(string radioLink)
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var song = await _service.ResolveSong(radioLink, Context.User.ToString(), MusicType.Radio);
|
||||
await InternalQueue(mp, song, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
public async Task Local([Remainder] string path)
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var song = await _service.ResolveSong(path, Context.User.ToString(), MusicType.Local);
|
||||
await InternalQueue(mp, song, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
public async Task LocalPl([Remainder] string dirPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dirPath))
|
||||
return;
|
||||
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
|
||||
DirectoryInfo dir;
|
||||
try { dir = new DirectoryInfo(dirPath); } catch { return; }
|
||||
var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories)
|
||||
.Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System) && x.Extension != ".jpg" && x.Extension != ".png");
|
||||
foreach (var file in fileEnum)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Yield();
|
||||
var song = await _service.ResolveSong(file.FullName, Context.User.ToString(), MusicType.Local);
|
||||
await InternalQueue(mp, song, true).ConfigureAwait(false);
|
||||
}
|
||||
catch (QueueFullException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Move()
|
||||
{
|
||||
var vch = ((IGuildUser)Context.User).VoiceChannel;
|
||||
|
||||
if (vch == null)
|
||||
return;
|
||||
|
||||
var mp = _service.GetPlayerOrDefault(Context.Guild.Id);
|
||||
|
||||
if (mp == null)
|
||||
return;
|
||||
|
||||
await mp.SetVoiceChannel(vch);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task MoveSong([Remainder] string fromto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fromto))
|
||||
return;
|
||||
|
||||
MusicPlayer mp = _service.GetPlayerOrDefault(Context.Guild.Id);
|
||||
if (mp == null)
|
||||
return;
|
||||
|
||||
fromto = fromto?.Trim();
|
||||
var fromtoArr = fromto.Split('>');
|
||||
|
||||
SongInfo s;
|
||||
if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out var n1) ||
|
||||
!int.TryParse(fromtoArr[1], out var n2) || n1 < 1 || n2 < 1 || n1 == n2
|
||||
|| (s = mp.MoveSong(--n1, --n2)) == null)
|
||||
{
|
||||
await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithTitle(s.Title.TrimTo(65))
|
||||
.WithUrl(s.SongUrl)
|
||||
.WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png"))
|
||||
.AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1 + 1}").WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2 + 1}").WithIsInline(true))
|
||||
.WithColor(NadekoBot.OkColor);
|
||||
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task SetMaxQueue(uint size = 0)
|
||||
{
|
||||
if (size < 0)
|
||||
return;
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
|
||||
mp.MaxQueueSize = size;
|
||||
|
||||
if (size == 0)
|
||||
await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false);
|
||||
else
|
||||
await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task SetMaxPlaytime(uint seconds)
|
||||
{
|
||||
if (seconds < 15 && seconds != 0)
|
||||
return;
|
||||
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
mp.MaxPlaytimeSeconds = seconds;
|
||||
if (seconds == 0)
|
||||
await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false);
|
||||
else
|
||||
await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task ReptCurSong()
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var (_, currentSong) = mp.Current;
|
||||
if (currentSong == null)
|
||||
return;
|
||||
var currentValue = mp.ToggleRepeatSong();
|
||||
|
||||
if (currentValue)
|
||||
await Context.Channel.EmbedAsync(new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithAuthor(eab => eab.WithMusicIcon().WithName("🔂 " + GetText("repeating_track")))
|
||||
.WithDescription(currentSong.PrettyName)
|
||||
.WithFooter(ef => ef.WithText(currentSong.PrettyInfo))).ConfigureAwait(false);
|
||||
else
|
||||
await Context.Channel.SendConfirmAsync("🔂 " + GetText("repeating_track_stopped"))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task RepeatPl()
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
var currentValue = mp.ToggleRepeatPlaylist();
|
||||
if (currentValue)
|
||||
await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false);
|
||||
else
|
||||
await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Autoplay()
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
|
||||
if (!mp.ToggleAutoplay())
|
||||
await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false);
|
||||
else
|
||||
await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[RequireUserPermission(GuildPermission.ManageMessages)]
|
||||
public async Task SetMusicChannel()
|
||||
{
|
||||
var mp = await _service.GetOrCreatePlayer(Context);
|
||||
|
||||
mp.OutputTextChannel = (ITextChannel)Context.Channel;
|
||||
|
||||
await ReplyConfirmLocalized("set_music_channel").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
239
NadekoBot.Core/Modules/Music/Services/MusicService.cs
Normal file
239
NadekoBot.Core/Modules/Music/Services/MusicService.cs
Normal file
@ -0,0 +1,239 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NLog;
|
||||
using System.IO;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Modules.Music.Common;
|
||||
using NadekoBot.Modules.Music.Common.Exceptions;
|
||||
using NadekoBot.Modules.Music.Common.SongResolver;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Services
|
||||
{
|
||||
public class MusicService : INService, IUnloadableService
|
||||
{
|
||||
public const string MusicDataPath = "data/musicdata";
|
||||
|
||||
private readonly IGoogleApiService _google;
|
||||
private readonly NadekoStrings _strings;
|
||||
private readonly ILocalization _localization;
|
||||
private readonly DbService _db;
|
||||
private readonly Logger _log;
|
||||
private readonly SoundCloudApiService _sc;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly ConcurrentDictionary<ulong, float> _defaultVolumes;
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public ConcurrentDictionary<ulong, MusicPlayer> MusicPlayers { get; } = new ConcurrentDictionary<ulong, MusicPlayer>();
|
||||
|
||||
public MusicService(DiscordSocketClient client, IGoogleApiService google,
|
||||
NadekoStrings strings, ILocalization localization, DbService db,
|
||||
SoundCloudApiService sc, IBotCredentials creds, NadekoBot bot)
|
||||
{
|
||||
_client = client;
|
||||
_google = google;
|
||||
_strings = strings;
|
||||
_localization = localization;
|
||||
_db = db;
|
||||
_sc = sc;
|
||||
_creds = creds;
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
_client.LeftGuild += _client_LeftGuild;
|
||||
|
||||
try { Directory.Delete(MusicDataPath, true); } catch { }
|
||||
|
||||
_defaultVolumes = new ConcurrentDictionary<ulong, float>(
|
||||
bot.AllGuildConfigs
|
||||
.ToDictionary(x => x.GuildId, x => x.DefaultMusicVolume));
|
||||
|
||||
Directory.CreateDirectory(MusicDataPath);
|
||||
}
|
||||
|
||||
public Task Unload()
|
||||
{
|
||||
_client.LeftGuild -= _client_LeftGuild;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task _client_LeftGuild(SocketGuild arg)
|
||||
{
|
||||
var t = DestroyPlayer(arg.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public float GetDefaultVolume(ulong guildId)
|
||||
{
|
||||
return _defaultVolumes.GetOrAdd(guildId, (id) =>
|
||||
{
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Task<MusicPlayer> GetOrCreatePlayer(ICommandContext context)
|
||||
{
|
||||
var gUsr = (IGuildUser)context.User;
|
||||
var txtCh = (ITextChannel)context.Channel;
|
||||
var vCh = gUsr.VoiceChannel;
|
||||
return GetOrCreatePlayer(context.Guild.Id, vCh, txtCh);
|
||||
}
|
||||
|
||||
public async Task<MusicPlayer> GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh)
|
||||
{
|
||||
string GetText(string text, params object[] replacements) =>
|
||||
_strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements);
|
||||
|
||||
_log.Info("Checks");
|
||||
if (voiceCh == null || voiceCh.Guild != textCh.Guild)
|
||||
{
|
||||
if (textCh != null)
|
||||
{
|
||||
await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false);
|
||||
}
|
||||
throw new NotInVoiceChannelException();
|
||||
}
|
||||
_log.Info("Get or add");
|
||||
return MusicPlayers.GetOrAdd(guildId, _ =>
|
||||
{
|
||||
_log.Info("Getting default volume");
|
||||
var vol = GetDefaultVolume(guildId);
|
||||
_log.Info("Creating musicplayer instance");
|
||||
var mp = new MusicPlayer(this, _google, voiceCh, textCh, vol);
|
||||
|
||||
IUserMessage playingMessage = null;
|
||||
IUserMessage lastFinishedMessage = null;
|
||||
|
||||
_log.Info("Subscribing");
|
||||
mp.OnCompleted += async (s, song) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
lastFinishedMessage?.DeleteAfter(0);
|
||||
|
||||
try
|
||||
{
|
||||
lastFinishedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithAuthor(eab => eab.WithName(GetText("finished_song")).WithMusicIcon())
|
||||
.WithDescription(song.PrettyName)
|
||||
.WithFooter(ef => ef.WithText(song.PrettyInfo)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
};
|
||||
mp.OnStarted += async (player, song) =>
|
||||
{
|
||||
//try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); }
|
||||
//catch
|
||||
//{
|
||||
// // ignored
|
||||
//}
|
||||
var sender = player;
|
||||
if (sender == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
playingMessage?.DeleteAfter(0);
|
||||
|
||||
playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithAuthor(eab => eab.WithName(GetText("playing_song", song.Index + 1)).WithMusicIcon())
|
||||
.WithDescription(song.Song.PrettyName)
|
||||
.WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.Song.PrettyInfo)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
};
|
||||
mp.OnPauseChanged += async (player, paused) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
IUserMessage msg;
|
||||
if (paused)
|
||||
msg = await mp.OutputTextChannel.SendConfirmAsync(GetText("paused")).ConfigureAwait(false);
|
||||
else
|
||||
msg = await mp.OutputTextChannel.SendConfirmAsync(GetText("resumed")).ConfigureAwait(false);
|
||||
|
||||
msg?.DeleteAfter(10);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
};
|
||||
_log.Info("Done creating");
|
||||
return mp;
|
||||
});
|
||||
}
|
||||
|
||||
public MusicPlayer GetPlayerOrDefault(ulong guildId)
|
||||
{
|
||||
if (MusicPlayers.TryGetValue(guildId, out var mp))
|
||||
return mp;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task TryQueueRelatedSongAsync(SongInfo song, ITextChannel txtCh, IVoiceChannel vch)
|
||||
{
|
||||
var related = (await _google.GetRelatedVideosAsync(song.VideoId, 4)).ToArray();
|
||||
if (!related.Any())
|
||||
return;
|
||||
|
||||
var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.YouTube);
|
||||
if (si == null)
|
||||
throw new SongNotFoundException();
|
||||
var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh);
|
||||
mp.Enqueue(si);
|
||||
}
|
||||
|
||||
public async Task<SongInfo> ResolveSong(string query, string queuerName, MusicType? musicType = null)
|
||||
{
|
||||
query.ThrowIfNull(nameof(query));
|
||||
|
||||
ISongResolverFactory resolverFactory = new SongResolverFactory(_sc);
|
||||
var strategy = await resolverFactory.GetResolveStrategy(query, musicType);
|
||||
var sinfo = await strategy.ResolveSong(query);
|
||||
|
||||
if (sinfo == null)
|
||||
return null;
|
||||
|
||||
sinfo.QueuerName = queuerName;
|
||||
|
||||
return sinfo;
|
||||
}
|
||||
|
||||
public async Task DestroyAllPlayers()
|
||||
{
|
||||
foreach (var key in MusicPlayers.Keys)
|
||||
{
|
||||
await DestroyPlayer(key);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DestroyPlayer(ulong id)
|
||||
{
|
||||
if (MusicPlayers.TryRemove(id, out var mp))
|
||||
await mp.Destroy();
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user