Merge pull request #1330 from Kwoth/music-rework

Music rework
This commit is contained in:
Master Kwoth 2017-07-04 23:52:34 +02:00 committed by GitHub
commit 507c9de136
25 changed files with 2580 additions and 1693 deletions

View File

@ -0,0 +1,116 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.DataStructures
{
public class PoopyRingBuffer : IDisposable
{
// readpos == writepos means empty
// writepos == readpos - 1 means full
private readonly byte[] buffer;
public int Capacity { get; }
private int _readPos = 0;
private int ReadPos
{
get => _readPos;
set => _readPos = value;
}
private int _writePos = 0;
private int WritePos
{
get => _writePos;
set => _writePos = value;
}
public int Length => ReadPos <= WritePos
? WritePos - ReadPos
: Capacity - (ReadPos - WritePos);
public int RemainingCapacity
{
get => Capacity - Length - 1;
}
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
public PoopyRingBuffer(int capacity = 81920 * 100)
{
this.Capacity = capacity + 1;
this.buffer = new byte[this.Capacity];
}
public int Read(byte[] b, int offset, int toRead)
{
if (WritePos == ReadPos)
return 0;
if (toRead > Length)
toRead = Length;
if (WritePos > ReadPos)
{
Array.Copy(buffer, ReadPos, b, offset, toRead);
ReadPos += toRead;
}
else
{
var toEnd = Capacity - ReadPos;
var firstRead = toRead > toEnd ?
toEnd :
toRead;
Array.Copy(buffer, ReadPos, b, offset, firstRead);
ReadPos += firstRead;
var secondRead = toRead - firstRead;
if (secondRead > 0)
{
Array.Copy(buffer, 0, b, offset + firstRead, secondRead);
ReadPos = secondRead;
}
}
return toRead;
}
public bool Write(byte[] b, int offset, int toWrite)
{
while (toWrite > RemainingCapacity)
return false;
if (toWrite == 0)
return true;
if (WritePos < ReadPos)
{
Array.Copy(b, offset, buffer, WritePos, toWrite);
WritePos += toWrite;
}
else
{
var toEnd = Capacity - WritePos;
var firstWrite = toWrite > toEnd ?
toEnd :
toWrite;
Array.Copy(b, offset, buffer, WritePos, firstWrite);
var secondWrite = toWrite - firstWrite;
if (secondWrite > 0)
{
Array.Copy(b, offset + firstWrite, buffer, 0, secondWrite);
WritePos = secondWrite;
}
else
{
WritePos += firstWrite;
if (WritePos == Capacity)
WritePos = 0;
}
}
return true;
}
public void Dispose()
{
}
}
}

View File

@ -74,25 +74,25 @@ namespace NadekoBot.DataStructures.Replacements
return this; return this;
} }
public ReplacementBuilder WithMusic(MusicService ms) //public ReplacementBuilder WithMusic(MusicService ms)
{ //{
_reps.TryAdd("%playing%", () => // _reps.TryAdd("%playing%", () =>
{ // {
var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); // var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null);
if (cnt != 1) return cnt.ToString(); // if (cnt != 1) return cnt.ToString();
try // try
{ // {
var mp = ms.MusicPlayers.FirstOrDefault(); // var mp = ms.MusicPlayers.FirstOrDefault();
return mp.Value.CurrentSong.SongInfo.Title; // return mp.Value.CurrentSong.SongInfo.Title;
} // }
catch // catch
{ // {
return "No songs"; // return "No songs";
} // }
}); // });
_reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()); // _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString());
return this; // return this;
} //}
public ReplacementBuilder WithRngRegex() public ReplacementBuilder WithRngRegex()
{ {

View File

@ -1,6 +1,7 @@
using NadekoBot.Extensions; using NadekoBot.Extensions;
using NadekoBot.Services; using NadekoBot.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
using NLog;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -19,9 +20,11 @@ namespace NadekoBot.DataStructures
private readonly ConcurrentDictionary<DapiSearchType, SemaphoreSlim> _locks = new ConcurrentDictionary<DapiSearchType, SemaphoreSlim>(); private readonly ConcurrentDictionary<DapiSearchType, SemaphoreSlim> _locks = new ConcurrentDictionary<DapiSearchType, SemaphoreSlim>();
private readonly SortedSet<ImageCacherObject> _cache; private readonly SortedSet<ImageCacherObject> _cache;
private readonly Logger _log;
public SearchImageCacher() public SearchImageCacher()
{ {
_log = LogManager.GetCurrentClassLogger();
_rng = new NadekoRandom(); _rng = new NadekoRandom();
_cache = new SortedSet<ImageCacherObject>(); _cache = new SortedSet<ImageCacherObject>();
} }
@ -85,7 +88,7 @@ namespace NadekoBot.DataStructures
public async Task<ImageCacherObject[]> DownloadImages(string tag, bool isExplicit, DapiSearchType type) public async Task<ImageCacherObject[]> DownloadImages(string tag, bool isExplicit, DapiSearchType type)
{ {
Console.WriteLine($"Loading extra images from {type}"); _log.Info($"Loading extra images from {type}");
tag = tag?.Replace(" ", "_").ToLowerInvariant(); tag = tag?.Replace(" ", "_").ToLowerInvariant();
if (isExplicit) if (isExplicit)
tag = "rating%3Aexplicit+" + tag; tag = "rating%3Aexplicit+" + tag;

View File

@ -0,0 +1,23 @@
using Discord.Commands;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.DataStructures
{
//public class SyncPrecondition : PreconditionAttribute
//{
// public override Task<PreconditionResult> CheckPermissions(ICommandContext context,
// CommandInfo command,
// IServiceProvider services)
// {
// }
//}
//public enum SyncType
//{
// Guild
//}
}

View File

@ -134,9 +134,8 @@ namespace NadekoBot.Modules.Administration
await user.RemoveRolesAsync(userRoles).ConfigureAwait(false); await user.RemoveRolesAsync(userRoles).ConfigureAwait(false);
await ReplyConfirmLocalized("rar", Format.Bold(user.ToString())).ConfigureAwait(false); await ReplyConfirmLocalized("rar", Format.Bold(user.ToString())).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception)
{ {
Console.WriteLine(ex);
await ReplyErrorLocalized("rar_err").ConfigureAwait(false); await ReplyErrorLocalized("rar_err").ConfigureAwait(false);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -173,7 +173,6 @@ namespace NadekoBot.Modules.Searches
// .FirstOrDefault(jt => jt["role"].ToString() == role)?["general"]; // .FirstOrDefault(jt => jt["role"].ToString() == role)?["general"];
// if (general == null) // if (general == null)
// { // {
// //Console.WriteLine("General is null.");
// return; // return;
// } // }
// //get build data for this role // //get build data for this role
@ -309,7 +308,6 @@ namespace NadekoBot.Modules.Searches
// } // }
// catch (Exception ex) // catch (Exception ex)
// { // {
// //Console.WriteLine(ex);
// await channel.SendMessageAsync("💢 Failed retreiving data for that champion.").ConfigureAwait(false); // await channel.SendMessageAsync("💢 Failed retreiving data for that champion.").ConfigureAwait(false);
// } // }
// }); // });

View File

@ -31,6 +31,7 @@ using NadekoBot.Extensions;
namespace NadekoBot namespace NadekoBot
{ {
//todo log when joining or leaving the server
public class NadekoBot public class NadekoBot
{ {
private Logger _log; private Logger _log;
@ -183,7 +184,7 @@ namespace NadekoBot
#endregion #endregion
var clashService = new ClashOfClansService(Client, Db, Localization, Strings, uow, startingGuildIdList); var clashService = new ClashOfClansService(Client, Db, Localization, Strings, uow, startingGuildIdList);
var musicService = new MusicService(GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); var musicService = new MusicService(Client, GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs);
var crService = new CustomReactionsService(permissionsService, Db, Strings, Client, CommandHandler, BotConfig, uow); var crService = new CustomReactionsService(permissionsService, Db, Strings, Client, CommandHandler, BotConfig, uow);
#region Games #region Games
@ -212,8 +213,6 @@ namespace NadekoBot
var pokemonService = new PokemonService(); var pokemonService = new PokemonService();
#endregion #endregion
//initialize Services //initialize Services
Services = new NServiceProvider.ServiceProviderBuilder() Services = new NServiceProvider.ServiceProviderBuilder()
.Add<ILocalization>(Localization) .Add<ILocalization>(Localization)
@ -269,7 +268,6 @@ namespace NadekoBot
.Add<NadekoBot>(this) .Add<NadekoBot>(this)
.Build(); .Build();
CommandHandler.AddServices(Services); CommandHandler.AddServices(Services);
//setup typereaders //setup typereaders
@ -352,7 +350,7 @@ namespace NadekoBot
#if GLOBAL_NADEKO #if GLOBAL_NADEKO
isPublicNadeko = true; isPublicNadeko = true;
#endif #endif
//Console.WriteLine(string.Join(", ", CommandService.Commands //_log.Info(string.Join(", ", CommandService.Commands
// .Distinct(x => x.Name + x.Module.Name) // .Distinct(x => x.Name + x.Module.Name)
// .SelectMany(x => x.Aliases) // .SelectMany(x => x.Aliases)
// .GroupBy(x => x) // .GroupBy(x => x)

View File

@ -1198,7 +1198,7 @@
<value>`{0}drawnew` or `{0}drawnew 5`</value> <value>`{0}drawnew` or `{0}drawnew 5`</value>
</data> </data>
<data name="shuffleplaylist_cmd" xml:space="preserve"> <data name="shuffleplaylist_cmd" xml:space="preserve">
<value>playlistshuffle plsh</value> <value>shuffle sh plsh</value>
</data> </data>
<data name="shuffleplaylist_desc" xml:space="preserve"> <data name="shuffleplaylist_desc" xml:space="preserve">
<value>Shuffles the current playlist.</value> <value>Shuffles the current playlist.</value>
@ -1467,6 +1467,15 @@
<data name="next_usage" xml:space="preserve"> <data name="next_usage" xml:space="preserve">
<value>`{0}n` or `{0}n 5`</value> <value>`{0}n` or `{0}n 5`</value>
</data> </data>
<data name="play_cmd" xml:space="preserve">
<value>play start</value>
</data>
<data name="play_desc" xml:space="preserve">
<value>If no arguments are specified, acts as `{0}next 1` command. If you specify a song number, it will jump to that song. If you specify a search query, acts as a `{0}q` command</value>
</data>
<data name="play_usage" xml:space="preserve">
<value>`{0}play` or `{0}play 5` or `{0}play Dream Of Venice`</value>
</data>
<data name="stop_cmd" xml:space="preserve"> <data name="stop_cmd" xml:space="preserve">
<value>stop s</value> <value>stop s</value>
</data> </data>

View File

@ -21,7 +21,10 @@ namespace NadekoBot.Services.Administration
TimeZoneInfo tz; TimeZoneInfo tz;
try try
{ {
tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId); if (x.TimeZoneId == null)
tz = null;
else
tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId);
} }
catch catch
{ {

View File

@ -1,6 +1,5 @@
using Discord.WebSocket; using Discord.WebSocket;
using NadekoBot.DataStructures.Replacements; using NadekoBot.DataStructures.Replacements;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
using NadekoBot.Services.Music; using NadekoBot.Services.Music;
using NLog; using NLog;
@ -35,7 +34,7 @@ namespace NadekoBot.Services.Administration
_rep = new ReplacementBuilder() _rep = new ReplacementBuilder()
.WithClient(client) .WithClient(client)
.WithStats(client) .WithStats(client)
.WithMusic(music) //.WithMusic(music)
.Build(); .Build();
_t = new Timer(async (objState) => _t = new Timer(async (objState) =>

View File

@ -12,7 +12,7 @@
public enum MusicType public enum MusicType
{ {
Radio, Radio,
Normal, YouTube,
Local, Local,
Soundcloud Soundcloud
} }

View File

@ -104,7 +104,6 @@ namespace NadekoBot.Services.Games
{ {
if (pc.Verbose) if (pc.Verbose)
{ {
//todo move this to permissions
var returnMsg = _strings.GetText("trigger", guild.Id, "Permissions".ToLowerInvariant(), index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))); var returnMsg = _strings.GetText("trigger", guild.Id, "Permissions".ToLowerInvariant(), index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild)));
try { await usrMsg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { } try { await usrMsg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { }
_log.Info(returnMsg); _log.Info(returnMsg);

View File

@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl
private readonly IBotCredentials _creds; private readonly IBotCredentials _creds;
private readonly DateTime _started; private readonly DateTime _started;
public const string BotVersion = "1.52"; public const string BotVersion = "1.53";
public string Author => "Kwoth#2560"; public string Author => "Kwoth#2560";
public string Library => "Discord.Net"; public string Library => "Discord.Net";

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Services.Impl
{
public class SyncPreconditionService
{
}
}

View File

@ -2,19 +2,27 @@
namespace NadekoBot.Services.Music namespace NadekoBot.Services.Music
{ {
class PlaylistFullException : Exception public class QueueFullException : Exception
{ {
public PlaylistFullException(string message) : base(message) public QueueFullException(string message) : base(message)
{ {
} }
public PlaylistFullException() : base("Queue is full.") { } public QueueFullException() : base("Queue is full.") { }
} }
class SongNotFoundException : Exception public class SongNotFoundException : Exception
{ {
public SongNotFoundException(string message) : base(message) public SongNotFoundException(string message) : base(message)
{ {
} }
public SongNotFoundException() : base("Song is not found.") { } public SongNotFoundException() : base("Song is not found.") { }
} }
public class NotInVoiceChannelException : Exception
{
public NotInVoiceChannelException(string message) : base(message)
{
}
public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { }
}
} }

View File

@ -1,381 +0,0 @@
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;
using NLog;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Services.Music
{
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>();
private readonly Logger _log;
private readonly IGoogleApiService _google;
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; }
public bool RepeatSong { get; private set; }
public bool RepeatPlaylist { get; private set; }
public bool Autoplay { get; set; }
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, IGoogleApiService google)
{
_log = LogManager.GetCurrentClassLogger();
_google = google;
OutputTextChannel = outputChannel;
Volume = defaultVolume ?? 1.0f;
PlaybackVoiceChannel = startingVoiceChannel ?? throw new ArgumentNullException(nameof(startingVoiceChannel));
SongCancelSource = new CancellationTokenSource();
CancelToken = SongCancelSource.Token;
Task.Run(async () =>
{
try
{
while (!Destroyed)
{
try
{
if (ActionQueue.TryDequeue(out Action action))
{
action();
}
}
finally
{
await Task.Delay(100).ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
_log.Warn("Action queue crashed");
_log.Warn(ex);
}
}).ConfigureAwait(false);
var t = new Thread(async () =>
{
while (!Destroyed)
{
try
{
CurrentSong = GetNextSong();
if (CurrentSong == null)
continue;
while (AudioClient?.ConnectionState == ConnectionState.Disconnecting ||
AudioClient?.ConnectionState == ConnectionState.Connecting)
{
_log.Info("Waiting for Audio client");
await Task.Delay(200).ConfigureAwait(false);
}
if (AudioClient == null || AudioClient.ConnectionState == ConnectionState.Disconnected)
AudioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false);
var index = _playlist.IndexOf(CurrentSong);
if (index != -1)
RemoveSongAt(index, true);
OnStarted(this, CurrentSong);
try
{
await CurrentSong.Play(AudioClient, CancelToken);
}
catch (OperationCanceledException)
{
}
finally
{
OnCompleted(this, CurrentSong);
}
if (RepeatPlaylist & !RepeatSong)
AddSong(CurrentSong, CurrentSong.QueuerName);
if (RepeatSong)
AddSong(CurrentSong, 0);
}
catch (Exception ex)
{
_log.Warn("Music thread almost crashed.");
_log.Warn(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;
Autoplay = 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 _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;
Autoplay = false;
Destroyed = true;
_playlist.Clear();
try { await AudioClient.StopAsync(); } 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() => RepeatSong = !RepeatSong;
public bool ToggleRepeatPlaylist() => RepeatPlaylist = !RepeatPlaylist;
public bool ToggleAutoplay() => Autoplay = !Autoplay;
public void ThrowIfQueueFull()
{
if (MaxQueueSize == 0)
return;
if (_playlist.Count >= MaxQueueSize)
throw new PlaylistFullException();
}
}
}

View File

@ -0,0 +1,593 @@
using Discord;
using Discord.Audio;
using System;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using System.Linq;
using System.Collections.Concurrent;
using NadekoBot.Extensions;
using System.Diagnostics;
namespace NadekoBot.Services.Music
{
public enum StreamState
{
Resolving,
Queued,
Playing,
Completed
}
public class MusicPlayer
{
private readonly Task _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; } = true;
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 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 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;
_player = Task.Run(async () =>
{
while (!Exited)
{
_bytesSent = 0;
CancellationToken cancelToken;
(int Index, SongInfo Song) data;
lock (locker)
{
data = Queue.Current;
cancelToken = SongCancelSource.Token;
manualSkip = false;
manualIndex = false;
}
if (data.Song == null)
continue;
_log.Info("Starting");
using (var b = new SongBuffer(data.Song.Uri, ""))
{
AudioOutStream pcm = null;
try
{
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;
}
var ac = await GetAudioClient();
if (ac == null)
{
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);
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");
}
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();
}
OnCompleted?.Invoke(this, data.Song);
}
}
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;
lock (locker)
queueCount = Queue.Count;
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 == Database.Models.MusicType.YouTube)
{
try
{
_log.Info("Loading related song");
await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel);
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)
{
Queue.Next();
}
}
}
}
}
catch (Exception ex)
{
_log.Error(ex);
}
do
{
await Task.Delay(500);
}
while ((Queue.Count == 0 || Stopped) && !Exited);
}
}, SongCancelSource.Token);
}
public void SetIndex(int index)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
lock (locker)
{
Queue.CurrentIndex = index;
manualIndex = true;
CancelCurrentSong();
}
}
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)
{
await t;
_audioClient.Dispose();
}
}
catch
{
}
newVoiceChannel = false;
var curUser = await VoiceChannel.Guild.GetCurrentUserAsync();
if (curUser.VoiceChannel != null)
{
var ac = await VoiceChannel.ConnectAsync();
await ac.StopAsync();
await Task.Delay(1000);
}
_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;
}
}
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)
Queue.Next(skipCount - 1);
Stopped = false;
Unpause();
CancelCurrentSong();
}
}
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;
if (cur.Index == index)
Next();
return Queue.RemoveAt(index);
}
}
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 == Database.Models.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));
}
}

View File

@ -0,0 +1,167 @@
using NadekoBot.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Services.Music
{
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 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;
for (int i = 0; i < Songs.Count; i++)
{
if (i == index)
{
Songs.Remove(current);
if (CurrentIndex != 0)
{
if (CurrentIndex >= index)
{
--CurrentIndex;
}
}
break;
}
}
return current.Value;
}
}
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 playlist = Songs.ToList();
if (n1 > playlist.Count || n2 > playlist.Count)
return null;
var s = playlist[n1 - 1];
playlist.Insert(n2 - 1, s);
var nn1 = n2 < n1 ? n1 : n1 - 1;
playlist.RemoveAt(nn1);
Songs = new LinkedList<SongInfo>(playlist);
return s;
}
}
}
}

View File

@ -5,12 +5,14 @@ using System.Threading.Tasks;
using Discord; using Discord;
using NadekoBot.Extensions; using NadekoBot.Extensions;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
using System.Text.RegularExpressions;
using NLog; using NLog;
using System.IO; using System.IO;
using VideoLibrary; using VideoLibrary;
using System.Net.Http;
using System.Collections.Generic; using System.Collections.Generic;
using Discord.Commands;
using Discord.WebSocket;
using System.Text.RegularExpressions;
using System.Net.Http;
namespace NadekoBot.Services.Music namespace NadekoBot.Services.Music
{ {
@ -26,13 +28,15 @@ namespace NadekoBot.Services.Music
private readonly SoundCloudApiService _sc; private readonly SoundCloudApiService _sc;
private readonly IBotCredentials _creds; private readonly IBotCredentials _creds;
private readonly ConcurrentDictionary<ulong, float> _defaultVolumes; private readonly ConcurrentDictionary<ulong, float> _defaultVolumes;
private readonly DiscordSocketClient _client;
public ConcurrentDictionary<ulong, MusicPlayer> MusicPlayers { get; } = new ConcurrentDictionary<ulong, MusicPlayer>(); public ConcurrentDictionary<ulong, MusicPlayer> MusicPlayers { get; } = new ConcurrentDictionary<ulong, MusicPlayer>();
public MusicService(IGoogleApiService google, public MusicService(DiscordSocketClient client, IGoogleApiService google,
NadekoStrings strings, ILocalization localization, DbService db, NadekoStrings strings, ILocalization localization, DbService db,
SoundCloudApiService sc, IBotCredentials creds, IEnumerable<GuildConfig> gcs) SoundCloudApiService sc, IBotCredentials creds, IEnumerable<GuildConfig> gcs)
{ {
_client = client;
_google = google; _google = google;
_strings = strings; _strings = strings;
_localization = localization; _localization = localization;
@ -48,28 +52,44 @@ namespace NadekoBot.Services.Music
Directory.CreateDirectory(MusicDataPath); Directory.CreateDirectory(MusicDataPath);
} }
public MusicPlayer GetPlayer(ulong guildId) public float GetDefaultVolume(ulong guildId)
{ {
MusicPlayers.TryGetValue(guildId, out var player); return _defaultVolumes.GetOrAdd(guildId, (id) =>
return player; {
using (var uow = _db.UnitOfWork)
{
return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume;
}
});
} }
public MusicPlayer GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh) 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) => string GetText(string text, params object[] replacements) =>
_strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements);
return MusicPlayers.GetOrAdd(guildId, server => if (voiceCh == null || voiceCh.Guild != textCh.Guild)
{ {
var vol = _defaultVolumes.GetOrAdd(guildId, (id) => if (textCh != null)
{ {
using (var uow = _db.UnitOfWork) await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false);
{ }
return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume; throw new ArgumentException(nameof(voiceCh));
} }
});
return MusicPlayers.GetOrAdd(guildId, _ =>
{
var vol = GetDefaultVolume(guildId);
var mp = new MusicPlayer(this, _google, voiceCh, textCh, vol);
var mp = new MusicPlayer(voiceCh, textCh, vol, _google);
IUserMessage playingMessage = null; IUserMessage playingMessage = null;
IUserMessage lastFinishedMessage = null; IUserMessage lastFinishedMessage = null;
mp.OnCompleted += async (s, song) => mp.OnCompleted += async (s, song) =>
@ -90,31 +110,19 @@ namespace NadekoBot.Services.Music
{ {
// ignored // ignored
} }
if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.ProviderType == MusicType.Normal)
{
var relatedVideos = (await _google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList();
if (relatedVideos.Count > 0)
await QueueSong(await textCh.Guild.GetCurrentUserAsync(),
textCh,
voiceCh,
relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)],
true).ConfigureAwait(false);
}
} }
catch catch
{ {
// ignored // ignored
} }
}; };
mp.OnStarted += async (player, song) => mp.OnStarted += async (player, song) =>
{ {
try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); }
catch //catch
{ //{
// ignored // // ignored
} //}
var sender = player; var sender = player;
if (sender == null) if (sender == null)
return; return;
@ -123,9 +131,9 @@ namespace NadekoBot.Services.Music
playingMessage?.DeleteAfter(0); playingMessage?.DeleteAfter(0);
playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithAuthor(eab => eab.WithName(GetText("playing_song")).WithMusicIcon()) .WithAuthor(eab => eab.WithName(GetText("playing_song", song.Index + 1)).WithMusicIcon())
.WithDescription(song.PrettyName) .WithDescription(song.Song.PrettyName)
.WithFooter(ef => ef.WithText(song.PrettyInfo))) .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.Song.PrettyInfo)))
.ConfigureAwait(false); .ConfigureAwait(false);
} }
catch catch
@ -133,7 +141,7 @@ namespace NadekoBot.Services.Music
// ignored // ignored
} }
}; };
mp.OnPauseChanged += async (paused) => mp.OnPauseChanged += async (player, paused) =>
{ {
try try
{ {
@ -151,194 +159,176 @@ namespace NadekoBot.Services.Music
} }
}; };
mp.SongRemoved += async (song, index) =>
{
try
{
var embed = new EmbedBuilder()
.WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon())
.WithDescription(song.PrettyName)
.WithFooter(ef => ef.WithText(song.PrettyInfo))
.WithErrorColor();
await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false);
}
catch
{
// ignored
}
};
return mp; return mp;
}); });
} }
public MusicPlayer GetPlayerOrDefault(ulong guildId)
public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal)
{ {
string GetText(string text, params object[] replacements) => if (MusicPlayers.TryGetValue(guildId, out var mp))
_strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); return mp;
else
if (voiceCh == null || voiceCh.Guild != textCh.Guild) return null;
{
if (!silent)
await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false);
throw new ArgumentNullException(nameof(voiceCh));
}
if (string.IsNullOrWhiteSpace(query) || query.Length < 3)
throw new ArgumentException("Invalid song query.", nameof(query));
var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh);
Song resolvedSong;
try
{
musicPlayer.ThrowIfQueueFull();
resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false);
if (resolvedSong == null)
throw new SongNotFoundException();
musicPlayer.AddSong(resolvedSong, queuer.Username);
}
catch (PlaylistFullException)
{
try
{
await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize));
}
catch
{
// ignored
}
throw;
}
if (!silent)
{
try
{
//var queuedMessage = await textCh.SendConfirmAsync($"🎵 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false);
var queuedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (musicPlayer.Playlist.Count + 1)).WithMusicIcon())
.WithDescription($"{resolvedSong.PrettyName}\n{GetText("queue")} ")
.WithThumbnailUrl(resolvedSong.Thumbnail)
.WithFooter(ef => ef.WithText(resolvedSong.PrettyProvider)))
.ConfigureAwait(false);
queuedMessage?.DeleteAfter(10);
}
catch
{
// ignored
} // if queued message sending fails, don't attempt to delete it
}
} }
public void DestroyPlayer(ulong id) public async Task TryQueueRelatedSongAsync(string query, ITextChannel txtCh, IVoiceChannel vch)
{ {
if (MusicPlayers.TryRemove(id, out var mp)) var related = (await _google.GetRelatedVideosAsync(query, 4)).ToArray();
mp.Destroy(); 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)
public async Task<Song> ResolveSong(string query, MusicType musicType = MusicType.Normal)
{ {
if (string.IsNullOrWhiteSpace(query)) query.ThrowIfNull(nameof(query));
throw new ArgumentNullException(nameof(query));
if (musicType != MusicType.Local && IsRadioLink(query)) SongInfo sinfo = null;
switch (musicType)
{ {
musicType = MusicType.Radio; case MusicType.YouTube:
query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; sinfo = await ResolveYoutubeSong(query, queuerName);
break;
case MusicType.Radio:
try { sinfo = ResolveRadioSong(IsRadioLink(query) ? await HandleStreamContainers(query) : query, queuerName); } catch { };
break;
case MusicType.Local:
sinfo = ResolveLocalSong(query, queuerName);
break;
case MusicType.Soundcloud:
sinfo = await ResolveSoundCloudSong(query, queuerName);
break;
case null:
if (_sc.IsSoundCloudLink(query))
sinfo = await ResolveSoundCloudSong(query, queuerName);
else if (IsRadioLink(query))
sinfo = ResolveRadioSong(await HandleStreamContainers(query), queuerName);
else
try
{
sinfo = await ResolveYoutubeSong(query, queuerName);
}
catch
{
sinfo = null;
}
break;
} }
try return sinfo;
}
public async Task<SongInfo> ResolveSoundCloudSong(string query, string queuerName)
{
var svideo = !_sc.IsSoundCloudLink(query) ?
await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false):
await _sc.ResolveVideoAsync(query).ConfigureAwait(false);
if (svideo == null)
return null;
return await SongInfoFromSVideo(svideo, queuerName);
}
public async Task<SongInfo> SongInfoFromSVideo(SoundCloudVideo svideo, string queuerName) =>
new SongInfo
{ {
switch (musicType) Title = svideo.FullName,
{ Provider = "SoundCloud",
case MusicType.Local: Uri = await svideo.StreamLink().ConfigureAwait(false),
return new Song(new SongInfo ProviderType = MusicType.Soundcloud,
{ Query = svideo.TrackLink,
Uri = "\"" + Path.GetFullPath(query) + "\"", AlbumArt = svideo.artwork_url,
Title = Path.GetFileNameWithoutExtension(query), QueuerName = queuerName
Provider = "Local File", };
ProviderType = musicType,
Query = query,
});
case MusicType.Radio:
return new Song(new SongInfo
{
Uri = query,
Title = $"{query}",
Provider = "Radio Stream",
ProviderType = musicType,
Query = query
})
{ TotalTime = TimeSpan.MaxValue };
}
if (_sc.IsSoundCloudLink(query))
{
var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false);
return new Song(new SongInfo
{
Title = svideo.FullName,
Provider = "SoundCloud",
Uri = await svideo.StreamLink(),
ProviderType = musicType,
Query = svideo.TrackLink,
AlbumArt = svideo.artwork_url,
})
{ TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) };
}
if (musicType == MusicType.Soundcloud) public SongInfo ResolveLocalSong(string query, string queuerName)
{ {
var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false); return new SongInfo
return new Song(new SongInfo
{
Title = svideo.FullName,
Provider = "SoundCloud",
Uri = await svideo.StreamLink(),
ProviderType = MusicType.Soundcloud,
Query = svideo.TrackLink,
AlbumArt = svideo.artwork_url,
})
{ TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) };
}
var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault();
if (string.IsNullOrWhiteSpace(link))
throw new OperationCanceledException("Not a valid youtube query.");
var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty<YouTubeVideo>(); } }).ConfigureAwait(false);
var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio);
var video = videos
.Where(v => v.AudioBitrate < 256)
.OrderByDescending(v => v.AudioBitrate)
.FirstOrDefault();
if (video == null) // do something with this error
throw new Exception("Could not load any video elements based on the query.");
var m = Regex.Match(query, @"\?t=(?<t>\d*)");
int gotoTime = 0;
if (m.Captures.Count > 0)
int.TryParse(m.Groups["t"].ToString(), out gotoTime);
var song = new Song(new SongInfo
{
Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube"
Provider = "YouTube",
Uri = await video.GetUriAsync().ConfigureAwait(false),
Query = link,
ProviderType = musicType,
});
song.SkipTo = gotoTime;
return song;
}
catch (Exception ex)
{ {
_log.Warn($"Failed resolving the link.{ex.Message}"); Uri = "\"" + Path.GetFullPath(query) + "\"",
_log.Warn(ex); Title = Path.GetFileNameWithoutExtension(query),
Provider = "Local File",
ProviderType = MusicType.Local,
Query = query,
QueuerName = queuerName
};
}
public SongInfo ResolveRadioSong(string query, string queuerName)
{
return new SongInfo
{
Uri = query,
Title = query,
Provider = "Radio Stream",
ProviderType = MusicType.Radio,
Query = query,
QueuerName = queuerName
};
}
public async Task<SongInfo> ResolveYoutubeSong(string query, string queuerName)
{
var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault();
if (string.IsNullOrWhiteSpace(link))
{
_log.Info("No song found.");
return null; return null;
} }
var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty<YouTubeVideo>(); } }).ConfigureAwait(false);
var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio);
var video = videos
.Where(v => v.AudioBitrate < 256)
.OrderByDescending(v => v.AudioBitrate)
.FirstOrDefault();
if (video == null) // do something with this error
{
_log.Info("Could not load any video elements based on the query.");
return null;
}
//var m = Regex.Match(query, @"\?t=(?<t>\d*)");
//int gotoTime = 0;
//if (m.Captures.Count > 0)
// int.TryParse(m.Groups["t"].ToString(), out gotoTime);
var song = new SongInfo
{
Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube"
Provider = "YouTube",
Uri = await video.GetUriAsync().ConfigureAwait(false),
Query = link,
ProviderType = MusicType.YouTube,
QueuerName = queuerName
};
return song;
} }
private bool IsRadioLink(string query) =>
(query.StartsWith("http") ||
query.StartsWith("ww"))
&&
(query.Contains(".pls") ||
query.Contains(".m3u") ||
query.Contains(".asx") ||
query.Contains(".xspf"));
public async Task DestroyPlayer(ulong id)
{
if (MusicPlayers.TryRemove(id, out var mp))
await mp.Destroy();
}
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 async Task<string> HandleStreamContainers(string query) private async Task<string> HandleStreamContainers(string query)
{ {
string file = null; string file = null;
@ -359,7 +349,7 @@ namespace NadekoBot.Services.Music
//Regex.Match(query) //Regex.Match(query)
try try
{ {
var m = Regex.Match(file, "File1=(?<url>.*?)\\n"); var m = plsRegex.Match(file);
var res = m.Groups["url"]?.ToString(); var res = m.Groups["url"]?.ToString();
return res?.Trim(); return res?.Trim();
} }
@ -378,7 +368,7 @@ namespace NadekoBot.Services.Music
*/ */
try try
{ {
var m = Regex.Match(file, "(?<url>^[^#].*)", RegexOptions.Multiline); var m = m3uRegex.Match(file);
var res = m.Groups["url"]?.ToString(); var res = m.Groups["url"]?.ToString();
return res?.Trim(); return res?.Trim();
} }
@ -394,7 +384,7 @@ namespace NadekoBot.Services.Music
//<ref href="http://armitunes.com:8000"/> //<ref href="http://armitunes.com:8000"/>
try try
{ {
var m = Regex.Match(file, "<ref href=\"(?<url>.*?)\""); var m = asxRegex.Match(file);
var res = m.Groups["url"]?.ToString(); var res = m.Groups["url"]?.ToString();
return res?.Trim(); return res?.Trim();
} }
@ -414,7 +404,7 @@ namespace NadekoBot.Services.Music
*/ */
try try
{ {
var m = Regex.Match(file, "<location>(?<url>.*?)</location>"); var m = xspfRegex.Match(file);
var res = m.Groups["url"]?.ToString(); var res = m.Groups["url"]?.ToString();
return res?.Trim(); return res?.Trim();
} }
@ -427,14 +417,5 @@ namespace NadekoBot.Services.Music
return query; return query;
} }
private bool IsRadioLink(string query) =>
(query.StartsWith("http") ||
query.StartsWith("ww"))
&&
(query.Contains(".pls") ||
query.Contains(".m3u") ||
query.Contains(".asx") ||
query.Contains(".xspf"));
} }
} }

View File

@ -1,296 +1,245 @@
using Discord.Audio; using NadekoBot.Extensions;
using NadekoBot.Extensions;
using NLog;
using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Net; using System.Net;
using Discord; using Discord;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
using System;
namespace NadekoBot.Services.Music namespace NadekoBot.Services.Music
{ {
public class SongInfo //public class Song
{ //{
public string Provider { get; set; } // public SongInfo SongInfo { get; }
public MusicType ProviderType { get; set; } // public MusicPlayer MusicPlayer { get; set; }
public string Query { get; set; }
public string Title { get; set; }
public string Uri { get; set; }
public string AlbumArt { get; set; }
}
public class Song // private string _queuerName;
{ // public string QueuerName { get{
public SongInfo SongInfo { get; } // return Format.Sanitize(_queuerName);
public MusicPlayer MusicPlayer { get; set; } // } set { _queuerName = value; } }
private string _queuerName; // public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
public string QueuerName { get{ // public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds));
return Format.Sanitize(_queuerName);
} set { _queuerName = value; } }
public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; // private const int _milliseconds = 20;
public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds)); // private const int _samplesPerFrame = (48000 / 1000) * _milliseconds;
// private const int _frameBytes = 3840; //16-bit, 2 channels
private const int _milliseconds = 20; // private ulong BytesSent { get; set; }
private const int _samplesPerFrame = (48000 / 1000) * _milliseconds;
private const int _frameBytes = 3840; //16-bit, 2 channels
private ulong BytesSent { get; set; } // //pwetty
//pwetty // public string PrettyProvider =>
// $"{(SongInfo.Provider ?? "???")}";
public string PrettyProvider => // public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime;
$"{(SongInfo.Provider ?? "???")}";
public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; // public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**";
public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**"; // public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}";
public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; // public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`";
public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`"; // public string PrettyCurrentTime {
// get {
// var time = CurrentTime.ToString(@"mm\:ss");
// var hrs = (int)CurrentTime.TotalHours;
public string PrettyCurrentTime { // if (hrs > 0)
get { // return hrs + ":" + time;
var time = CurrentTime.ToString(@"mm\:ss"); // else
var hrs = (int)CurrentTime.TotalHours; // return time;
// }
// }
if (hrs > 0) // public string PrettyTotalTime {
return hrs + ":" + time; // get
else // {
return time; // if (TotalTime == TimeSpan.Zero)
} // return "(?)";
} // if (TotalTime == TimeSpan.MaxValue)
// return "∞";
// var time = TotalTime.ToString(@"mm\:ss");
// var hrs = (int)TotalTime.TotalHours;
public string PrettyTotalTime { // if (hrs > 0)
get // return hrs + ":" + time;
{ // return time;
if (TotalTime == TimeSpan.Zero) // }
return "(?)"; // }
if (TotalTime == TimeSpan.MaxValue)
return "∞";
var time = TotalTime.ToString(@"mm\:ss");
var hrs = (int)TotalTime.TotalHours;
if (hrs > 0) // public string Thumbnail {
return hrs + ":" + time; // get {
return time; // switch (SongInfo.ProviderType)
} // {
} // case MusicType.Radio:
// return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links
// case MusicType.Normal:
// //todo 50 have videoid in songinfo from the start
// var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+");
// return $"https://img.youtube.com/vi/{ videoId }/0.jpg";
// case MusicType.Local:
// return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links
// case MusicType.Soundcloud:
// return SongInfo.AlbumArt;
// default:
// return "";
// }
// }
// }
public string Thumbnail { // public string SongUrl {
get { // get {
switch (SongInfo.ProviderType) // switch (SongInfo.ProviderType)
{ // {
case MusicType.Radio: // case MusicType.Normal:
return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links // return SongInfo.Query;
case MusicType.Normal: // case MusicType.Soundcloud:
//todo 50 have videoid in songinfo from the start // return SongInfo.Query;
var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+"); // case MusicType.Local:
return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; // return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }";
case MusicType.Local: // case MusicType.Radio:
return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links // return $"https://google.com/search?q={SongInfo.Title}";
case MusicType.Soundcloud: // default:
return SongInfo.AlbumArt; // return "";
default: // }
return ""; // }
} // }
}
}
public string SongUrl { // private readonly Logger _log;
get {
switch (SongInfo.ProviderType)
{
case MusicType.Normal:
return SongInfo.Query;
case MusicType.Soundcloud:
return SongInfo.Query;
case MusicType.Local:
return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }";
case MusicType.Radio:
return $"https://google.com/search?q={SongInfo.Title}";
default:
return "";
}
}
}
public int SkipTo { get; set; } // public Song(SongInfo songInfo)
// {
// SongInfo = songInfo;
// _log = LogManager.GetCurrentClassLogger();
// }
private readonly Logger _log; // public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
// {
// BytesSent = (ulong) SkipTo * 3840 * 50;
// var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString());
public Song(SongInfo songInfo) // var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100);
{ // var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false);
SongInfo = songInfo;
_log = LogManager.GetCurrentClassLogger();
}
public Song Clone() // try
{ // {
var s = new Song(SongInfo) // var attempt = 0;
{
MusicPlayer = MusicPlayer,
QueuerName = QueuerName
};
return s;
}
public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) // var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy
{ // var finished = false;
BytesSent = (ulong) SkipTo * 3840 * 50; // var count = 0;
var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString()); // var sw = new Stopwatch();
// var slowconnection = false;
// sw.Start();
// while (!finished)
// {
// var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken));
// if (t != prebufferingTask)
// {
// count++;
// if (count == 10)
// {
// slowconnection = true;
// prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB());
// _log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud");
// continue;
// }
var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100); // if (inStream.BufferingCompleted && count == 1)
var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false); // {
// _log.Debug("Prebuffering canceled. Cannot get any data from the stream.");
// return;
// }
// else
// {
// continue;
// }
// }
// else if (prebufferingTask.IsCanceled)
// {
// _log.Debug("Prebuffering canceled. Cannot get any data from the stream.");
// return;
// }
// finished = true;
// }
// sw.Stop();
// _log.Debug("Prebuffering successfully completed in " + sw.Elapsed);
try // var outStream = voiceClient.CreatePCMStream(AudioApplication.Music);
{
var attempt = 0;
var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy // int nextTime = Environment.TickCount + _milliseconds;
var finished = false;
var count = 0;
var sw = new Stopwatch();
var slowconnection = false;
sw.Start();
while (!finished)
{
var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken));
if (t != prebufferingTask)
{
count++;
if (count == 10)
{
slowconnection = true;
prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB());
_log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud");
continue;
}
if (inStream.BufferingCompleted && count == 1) // byte[] buffer = new byte[_frameBytes];
{ // while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason
_log.Debug("Prebuffering canceled. Cannot get any data from the stream."); // !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime
return; // {
} // var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
else // //await inStream.CopyToAsync(voiceClient.OutputStream);
{ // if (read < _frameBytes)
continue; // _log.Debug("read {0}", read);
} // unchecked
} // {
else if (prebufferingTask.IsCanceled) // BytesSent += (ulong)read;
{ // }
_log.Debug("Prebuffering canceled. Cannot get any data from the stream."); // if (read < _frameBytes)
return; // {
} // if (read == 0)
finished = true; // {
} // if (inStream.BufferingCompleted)
sw.Stop(); // break;
_log.Debug("Prebuffering successfully completed in " + sw.Elapsed); // if (attempt++ == 20)
// {
// MusicPlayer.SongCancelSource.Cancel();
// break;
// }
// if (slowconnection)
// {
// _log.Warn("Slow connection has disrupted music, waiting a bit for buffer");
var outStream = voiceClient.CreatePCMStream(AudioApplication.Music); // await Task.Delay(1000, cancelToken).ConfigureAwait(false);
// nextTime = Environment.TickCount + _milliseconds;
// }
// else
// {
// await Task.Delay(100, cancelToken).ConfigureAwait(false);
// nextTime = Environment.TickCount + _milliseconds;
// }
// }
// else
// attempt = 0;
// }
// else
// attempt = 0;
int nextTime = Environment.TickCount + _milliseconds; // while (MusicPlayer.Paused)
// {
byte[] buffer = new byte[_frameBytes]; // await Task.Delay(200, cancelToken).ConfigureAwait(false);
while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason // nextTime = Environment.TickCount + _milliseconds;
!(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime // }
{
//Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------");
var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
//await inStream.CopyToAsync(voiceClient.OutputStream);
if (read < _frameBytes)
_log.Debug("read {0}", read);
unchecked
{
BytesSent += (ulong)read;
}
if (read < _frameBytes)
{
if (read == 0)
{
if (inStream.BufferingCompleted)
break;
if (attempt++ == 20)
{
MusicPlayer.SongCancelSource.Cancel();
break;
}
if (slowconnection)
{
_log.Warn("Slow connection has disrupted music, waiting a bit for buffer");
await Task.Delay(1000, cancelToken).ConfigureAwait(false);
nextTime = Environment.TickCount + _milliseconds;
}
else
{
await Task.Delay(100, cancelToken).ConfigureAwait(false);
nextTime = Environment.TickCount + _milliseconds;
}
}
else
attempt = 0;
}
else
attempt = 0;
while (MusicPlayer.Paused)
{
await Task.Delay(200, cancelToken).ConfigureAwait(false);
nextTime = Environment.TickCount + _milliseconds;
}
buffer = AdjustVolume(buffer, MusicPlayer.Volume); // buffer = AdjustVolume(buffer, MusicPlayer.Volume);
if (read != _frameBytes) continue; // if (read != _frameBytes) continue;
nextTime = unchecked(nextTime + _milliseconds); // nextTime = unchecked(nextTime + _milliseconds);
int delayMillis = unchecked(nextTime - Environment.TickCount); // int delayMillis = unchecked(nextTime - Environment.TickCount);
if (delayMillis > 0) // if (delayMillis > 0)
await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false); // await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false);
await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); // await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false);
} // }
} // }
finally // finally
{ // {
await bufferTask; // await bufferTask;
inStream.Dispose(); // inStream.Dispose();
} // }
} // }
private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size) // private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size)
{ // {
while (!inStream.BufferingCompleted && inStream.Length < size) // while (!inStream.BufferingCompleted && inStream.Length < size)
{ // {
await Task.Delay(100, cancelToken); // await Task.Delay(100, cancelToken);
} // }
_log.Debug("Buffering successfull"); // _log.Debug("Buffering successfull");
} // }
//}
//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;
}
}
} }

View File

@ -1,4 +1,4 @@
using NadekoBot.Extensions; using NadekoBot.DataStructures;
using NLog; using NLog;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
@ -8,212 +8,378 @@ using System.Threading.Tasks;
namespace NadekoBot.Services.Music namespace NadekoBot.Services.Music
{ {
/// <summary> public class SongBuffer : IDisposable
/// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space.
/// It also help for large music by deleting files that are already seen.
/// </summary>
class SongBuffer : Stream
{ {
public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize) const int readSize = 81920;
private Process p;
private PoopyRingBuffer _outStream = new PoopyRingBuffer();
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private readonly Logger _log;
public string SongUri { get; private set; }
//private volatile bool restart = false;
public SongBuffer(string songUri, string skipTo)
{ {
MusicPlayer = musicPlayer;
Basename = basename;
SongInfo = songInfo;
SkipTo = skipTo;
MaxFileSize = maxFileSize;
CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
_log = LogManager.GetCurrentClassLogger(); _log = LogManager.GetCurrentClassLogger();
this.SongUri = songUri;
this.p = StartFFmpegProcess(songUri, 0);
var t = Task.Run(() =>
{
this.p.BeginErrorReadLine();
this.p.ErrorDataReceived += P_ErrorDataReceived;
this.p.WaitForExit();
});
} }
MusicPlayer MusicPlayer { get; } private Process StartFFmpegProcess(string songUri, float skipTo = 0)
{
return Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-ss {skipTo:F4} -err_detect ignore_err -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
});
}
private string Basename { get; } private void P_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.Data))
return;
_log.Error(">>> " + e.Data);
if (e.Data?.Contains("Error in the pull function") == true)
{
_log.Error("Ignore this.");
//restart = true;
}
}
private SongInfo SongInfo { get; } private readonly object locker = new object();
public Task<bool> StartBuffering(CancellationToken cancelToken)
{
var toReturn = new TaskCompletionSource<bool>();
var _ = Task.Run(async () =>
{
//int maxLoopsPerSec = 25;
var sw = Stopwatch.StartNew();
//var delay = 1000 / maxLoopsPerSec;
int currentLoops = 0;
int _bytesSent = 0;
try
{
//do
//{
// if (restart)
// {
// var cur = _bytesSent / 3840 / (1000 / 20.0f);
// _log.Info("Restarting");
// try { this.p.StandardOutput.Dispose(); } catch { }
// try { this.p.Dispose(); } catch { }
// this.p = StartFFmpegProcess(SongUri, cur);
// }
// restart = false;
++currentLoops;
byte[] buffer = new byte[readSize];
int bytesRead = 1;
while (!cancelToken.IsCancellationRequested && !this.p.HasExited)
{
bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false);
_bytesSent += bytesRead;
if (bytesRead == 0)
break;
bool written;
do
{
lock (locker)
written = _outStream.Write(buffer, 0, bytesRead);
if (!written)
await Task.Delay(2000, cancelToken);
}
while (!written && !cancelToken.IsCancellationRequested);
lock (locker)
if (_outStream.Length > 200_000 || bytesRead == 0)
if (toReturn.TrySetResult(true))
_log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2"));
private int SkipTo { get; } //_log.Info(_outStream.Length);
await Task.Delay(10);
private int MaxFileSize { get; } = 2.MiB(); }
//if (cancelToken.IsCancellationRequested)
private long FileNumber = -1; // _log.Info("Song canceled");
//else if (p.HasExited)
private long NextFileToRead = 0; // _log.Info("Song buffered completely (FFmpeg exited)");
//else if (bytesRead == 0)
public bool BufferingCompleted { get; private set; } = false; // _log.Info("Nothing read");
//}
private ulong CurrentBufferSize = 0; //while (restart && !cancelToken.IsCancellationRequested);
}
private FileStream CurrentFileStream; catch (System.ComponentModel.Win32Exception)
private Logger _log; {
_log.Error(@"You have not properly installed or configured FFMPEG.
public Task BufferSong(CancellationToken cancelToken) =>
Task.Run(async () =>
{
Process p = null;
FileStream outStream = null;
try
{
p = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = false,
CreateNoWindow = true,
});
byte[] buffer = new byte[81920];
int currentFileSize = 0;
ulong prebufferSize = 100ul.MiB();
outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
while (!p.HasExited) //Also fix low bandwidth
{
int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false);
if (currentFileSize >= MaxFileSize)
{
try
{
outStream.Dispose();
}
catch { }
outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
currentFileSize = bytesRead;
}
else
{
currentFileSize += bytesRead;
}
CurrentBufferSize += Convert.ToUInt64(bytesRead);
await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
while (CurrentBufferSize > prebufferSize)
await Task.Delay(100, cancelToken);
}
BufferingCompleted = true;
}
catch (System.ComponentModel.Win32Exception)
{
var oldclr = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(@"You have not properly installed or configured FFMPEG.
Please install and configure FFMPEG to play music. Please install and configure FFMPEG to play music.
Check the guides for your platform on how to setup ffmpeg correctly: Check the guides for your platform on how to setup ffmpeg correctly:
Windows Guide: https://goo.gl/OjKk8F Windows Guide: https://goo.gl/OjKk8F
Linux Guide: https://goo.gl/ShjCUo"); Linux Guide: https://goo.gl/ShjCUo");
Console.ForegroundColor = oldclr;
}
catch (Exception ex)
{
Console.WriteLine($"Buffering stopped: {ex.Message}");
}
finally
{
if (outStream != null)
outStream.Dispose();
Console.WriteLine($"Buffering done.");
if (p != null)
{
try
{
p.Kill();
}
catch { }
p.Dispose();
}
}
});
/// <summary>
/// Return the next file to read, and delete the old one
/// </summary>
/// <returns>Name of the file to read</returns>
private string GetNextFile()
{
string filename = Basename + "-" + NextFileToRead;
if (NextFileToRead != 0)
{
try
{
CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length);
File.Delete(Basename + "-" + (NextFileToRead - 1));
} }
catch { } catch (OperationCanceledException) { }
} catch (InvalidOperationException) { } // when ffmpeg is disposed
NextFileToRead++; catch (Exception ex)
return filename;
}
private bool IsNextFileReady()
{
return NextFileToRead <= FileNumber;
}
private void CleanFiles()
{
for (long i = NextFileToRead - 1; i <= FileNumber; i++)
{
try
{ {
File.Delete(Basename + "-" + i); _log.Info(ex);
} }
catch { } finally
}
}
//Stream part
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => (long)CurrentBufferSize;
public override long Position { get; set; } = 0;
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count)
{
int read = CurrentFileStream.Read(buffer, offset, count);
if (read < count)
{
if (!BufferingCompleted || IsNextFileReady())
{ {
CurrentFileStream.Dispose(); if (toReturn.TrySetResult(false))
CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); _log.Info("Prebuffering failed");
read += CurrentFileStream.Read(buffer, read + offset, count - read);
} }
if (read < count) }, cancelToken);
Array.Clear(buffer, read, count - read);
return toReturn.Task;
}
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();
} }
return read; catch (Exception ex)
} {
_log.Error(ex);
public override long Seek(long offset, SeekOrigin origin) }
{ try
throw new NotImplementedException(); {
} if(!this.p.HasExited)
this.p.Kill();
public override void SetLength(long value) }
{ catch
throw new NotImplementedException(); {
} }
_outStream.Dispose();
public override void Write(byte[] buffer, int offset, int count) this.p.Dispose();
{
throw new NotImplementedException();
}
public new void Dispose()
{
CurrentFileStream.Dispose();
MusicPlayer.SongCancelSource.Cancel();
CleanFiles();
base.Dispose();
} }
} }
} }
//namespace NadekoBot.Services.Music
//{
// /// <summary>
// /// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space.
// /// It also help for large music by deleting files that are already seen.
// /// </summary>
// class SongBuffer : Stream
// {
// public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize)
// {
// MusicPlayer = musicPlayer;
// Basename = basename;
// SongInfo = songInfo;
// SkipTo = skipTo;
// MaxFileSize = maxFileSize;
// CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
// _log = LogManager.GetCurrentClassLogger();
// }
// MusicPlayer MusicPlayer { get; }
// private string Basename { get; }
// private SongInfo SongInfo { get; }
// private int SkipTo { get; }
// private int MaxFileSize { get; } = 2.MiB();
// private long FileNumber = -1;
// private long NextFileToRead = 0;
// public bool BufferingCompleted { get; private set; } = false;
// private ulong CurrentBufferSize = 0;
// private FileStream CurrentFileStream;
// private Logger _log;
// public Task BufferSong(CancellationToken cancelToken) =>
// Task.Run(async () =>
// {
// Process p = null;
// FileStream outStream = null;
// try
// {
// p = Process.Start(new ProcessStartInfo
// {
// FileName = "ffmpeg",
// Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet",
// UseShellExecute = false,
// RedirectStandardOutput = true,
// RedirectStandardError = false,
// CreateNoWindow = true,
// });
// byte[] buffer = new byte[81920];
// int currentFileSize = 0;
// ulong prebufferSize = 100ul.MiB();
// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
// while (!p.HasExited) //Also fix low bandwidth
// {
// int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false);
// if (currentFileSize >= MaxFileSize)
// {
// try
// {
// outStream.Dispose();
// }
// catch { }
// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
// currentFileSize = bytesRead;
// }
// else
// {
// currentFileSize += bytesRead;
// }
// CurrentBufferSize += Convert.ToUInt64(bytesRead);
// await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
// while (CurrentBufferSize > prebufferSize)
// await Task.Delay(100, cancelToken);
// }
// BufferingCompleted = true;
// }
// catch (System.ComponentModel.Win32Exception)
// {
// var oldclr = Console.ForegroundColor;
// Console.ForegroundColor = ConsoleColor.Red;
// Console.WriteLine(@"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");
// Console.ForegroundColor = oldclr;
// }
// catch (Exception ex)
// {
// Console.WriteLine($"Buffering stopped: {ex.Message}");
// }
// finally
// {
// if (outStream != null)
// outStream.Dispose();
// if (p != null)
// {
// try
// {
// p.Kill();
// }
// catch { }
// p.Dispose();
// }
// }
// });
// /// <summary>
// /// Return the next file to read, and delete the old one
// /// </summary>
// /// <returns>Name of the file to read</returns>
// private string GetNextFile()
// {
// string filename = Basename + "-" + NextFileToRead;
// if (NextFileToRead != 0)
// {
// try
// {
// CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length);
// File.Delete(Basename + "-" + (NextFileToRead - 1));
// }
// catch { }
// }
// NextFileToRead++;
// return filename;
// }
// private bool IsNextFileReady()
// {
// return NextFileToRead <= FileNumber;
// }
// private void CleanFiles()
// {
// for (long i = NextFileToRead - 1; i <= FileNumber; i++)
// {
// try
// {
// File.Delete(Basename + "-" + i);
// }
// catch { }
// }
// }
// //Stream part
// public override bool CanRead => true;
// public override bool CanSeek => false;
// public override bool CanWrite => false;
// public override long Length => (long)CurrentBufferSize;
// public override long Position { get; set; } = 0;
// public override void Flush() { }
// public override int Read(byte[] buffer, int offset, int count)
// {
// int read = CurrentFileStream.Read(buffer, offset, count);
// if (read < count)
// {
// if (!BufferingCompleted || IsNextFileReady())
// {
// CurrentFileStream.Dispose();
// CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
// read += CurrentFileStream.Read(buffer, read + offset, count - read);
// }
// if (read < count)
// Array.Clear(buffer, read, count - read);
// }
// return read;
// }
// public override long Seek(long offset, SeekOrigin origin)
// {
// throw new NotImplementedException();
// }
// public override void SetLength(long value)
// {
// throw new NotImplementedException();
// }
// public override void Write(byte[] buffer, int offset, int count)
// {
// throw new NotImplementedException();
// }
// public new void Dispose()
// {
// CurrentFileStream.Dispose();
// MusicPlayer.SongCancelSource.Cancel();
// CleanFiles();
// base.Dispose();
// }
// }
//}

View File

@ -0,0 +1,98 @@
using Discord;
using NadekoBot.Extensions;
using NadekoBot.Services.Database.Models;
using System;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace NadekoBot.Services.Music
{
public class SongInfo
{
public string Provider { get; set; }
public MusicType ProviderType { get; set; }
public string Query { get; set; }
public string Title { get; set; }
public string Uri { get; set; }
public string AlbumArt { 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);
public string Thumbnail
{
get
{
switch (ProviderType)
{
case MusicType.Radio:
return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links
case MusicType.YouTube:
return $"https://img.youtube.com/vi/{ VideoId }/0.jpg";
case MusicType.Local:
return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links
case MusicType.Soundcloud:
return AlbumArt;
default:
return "";
}
}
}
}
}

View File

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Services.Music
{
public class SongResolver
{
// public async Task<Song> ResolveSong(string query, MusicType musicType = MusicType.Normal)
// {
// if (string.IsNullOrWhiteSpace(query))
// throw new ArgumentNullException(nameof(query));
// if (musicType != MusicType.Local && IsRadioLink(query))
// {
// musicType = MusicType.Radio;
// query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query;
// }
// try
// {
// switch (musicType)
// {
// case MusicType.Local:
// return new Song(new SongInfo
// {
// Uri = "\"" + Path.GetFullPath(query) + "\"",
// Title = Path.GetFileNameWithoutExtension(query),
// Provider = "Local File",
// ProviderType = musicType,
// Query = query,
// });
// case MusicType.Radio:
// return new Song(new SongInfo
// {
// Uri = query,
// Title = $"{query}",
// Provider = "Radio Stream",
// ProviderType = musicType,
// Query = query
// })
// { TotalTime = TimeSpan.MaxValue };
// }
// if (_sc.IsSoundCloudLink(query))
// {
// var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false);
// return new Song(new SongInfo
// {
// Title = svideo.FullName,
// Provider = "SoundCloud",
// Uri = await svideo.StreamLink(),
// ProviderType = musicType,
// Query = svideo.TrackLink,
// AlbumArt = svideo.artwork_url,
// })
// { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) };
// }
// if (musicType == MusicType.Soundcloud)
// {
// var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false);
// return new Song(new SongInfo
// {
// Title = svideo.FullName,
// Provider = "SoundCloud",
// Uri = await svideo.StreamLink(),
// ProviderType = MusicType.Soundcloud,
// Query = svideo.TrackLink,
// AlbumArt = svideo.artwork_url,
// })
// { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) };
// }
// var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault();
// if (string.IsNullOrWhiteSpace(link))
// throw new OperationCanceledException("Not a valid youtube query.");
// var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty<YouTubeVideo>(); } }).ConfigureAwait(false);
// var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio);
// var video = videos
// .Where(v => v.AudioBitrate < 256)
// .OrderByDescending(v => v.AudioBitrate)
// .FirstOrDefault();
// if (video == null) // do something with this error
// throw new Exception("Could not load any video elements based on the query.");
// var m = Regex.Match(query, @"\?t=(?<t>\d*)");
// int gotoTime = 0;
// if (m.Captures.Count > 0)
// int.TryParse(m.Groups["t"].ToString(), out gotoTime);
// var song = new Song(new SongInfo
// {
// Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube"
// Provider = "YouTube",
// Uri = await video.GetUriAsync().ConfigureAwait(false),
// Query = link,
// ProviderType = musicType,
// });
// song.SkipTo = gotoTime;
// return song;
// }
// catch (Exception ex)
// {
// _log.Warn($"Failed resolving the link.{ex.Message}");
// _log.Warn(ex);
// return null;
// }
// }
}
}

View File

@ -404,9 +404,10 @@
"music_attempting_to_queue": "Attempting to queue {0} songs...", "music_attempting_to_queue": "Attempting to queue {0} songs...",
"music_autoplay_disabled": "Autoplay disabled.", "music_autoplay_disabled": "Autoplay disabled.",
"music_autoplay_enabled": "Autoplay enabled.", "music_autoplay_enabled": "Autoplay enabled.",
"music_autoplaying": "Auto-playing.",
"music_defvol_set": "Default volume set to {0}%", "music_defvol_set": "Default volume set to {0}%",
"music_dir_queue_complete": "Directory queue complete.", "music_dir_queue_complete": "Directory queue complete.",
"music_fairplay": "fairplay", "music_fairplay": "Fairplay",
"music_finished_song": "Finished song", "music_finished_song": "Finished song",
"music_fp_disabled": "Fair play disabled.", "music_fp_disabled": "Fair play disabled.",
"music_fp_enabled": "Fair play enabled.", "music_fp_enabled": "Fair play enabled.",
@ -424,7 +425,7 @@
"music_no_search_results": "No search results.", "music_no_search_results": "No search results.",
"music_paused": "Music playback paused.", "music_paused": "Music playback paused.",
"music_player_queue": "Player queue - Page {0}/{1}", "music_player_queue": "Player queue - Page {0}/{1}",
"music_playing_song": "Playing song", "music_playing_song": "Playing song #{0}",
"music_playlists": "`#{0}` - **{1}** by *{2}* ({3} songs)", "music_playlists": "`#{0}` - **{1}** by *{2}* ({3} songs)",
"music_playlists_page": "Page {0} of saved playlists", "music_playlists_page": "Page {0} of saved playlists",
"music_playlist_deleted": "Playlist deleted.", "music_playlist_deleted": "Playlist deleted.",
@ -437,19 +438,24 @@
"music_queued_song": "Queued song", "music_queued_song": "Queued song",
"music_queue_cleared": "Music queue cleared.", "music_queue_cleared": "Music queue cleared.",
"music_queue_full": "Queue is full at {0}/{0}.", "music_queue_full": "Queue is full at {0}/{0}.",
"music_queue_stopped": "Player is stopped. Use {0} command to start playing.",
"music_removed_song": "Removed song", "music_removed_song": "Removed song",
"music_removed_song_error": "Song on that index doesn't exist",
"music_repeating_cur_song": "Repeating current song", "music_repeating_cur_song": "Repeating current song",
"music_repeating_playlist": "Repeating playlist", "music_repeating_playlist": "Repeating playlist",
"music_repeating_track": "Repeating track", "music_repeating_track": "Repeating track",
"music_repeating_track_stopped": "Current track repeat stopped.", "music_repeating_track_stopped": "Current track repeat stopped.",
"music_shuffling_playlist": "Shuffling songs",
"music_resumed": "Music playback resumed.", "music_resumed": "Music playback resumed.",
"music_rpl_disabled": "Repeat playlist disabled.", "music_rpl_disabled": "Repeat playlist disabled.",
"music_rpl_enabled": "Repeat playlist enabled.", "music_rpl_enabled": "Repeat playlist enabled.",
"music_set_music_channel": "I will now output playing, finished, paused and removed songs in this channel.", "music_set_music_channel": "I will now output playing, finished, paused and removed songs in this channel.",
"music_skipped_to": "Skipped to `{0}:{1}`", "music_skipped_to": "Skipped to `{0}:{1}`",
"music_songs_shuffled": "Songs shuffled", "music_songs_shuffle_enable": "Songs will shuffle from now on.",
"music_songs_shuffle_disable": "Songs will no longer shuffle.",
"music_song_moved": "Song moved", "music_song_moved": "Song moved",
"music_song_not_found": "No song found.", "music_song_not_found": "No song found.",
"music_song_skips_after": "Songs will skip after {0}",
"music_time_format": "{0}h {1}m {2}s", "music_time_format": "{0}h {1}m {2}s",
"music_to_position": "To position", "music_to_position": "To position",
"music_unlimited": "unlimited", "music_unlimited": "unlimited",