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;
}
public ReplacementBuilder WithMusic(MusicService ms)
{
_reps.TryAdd("%playing%", () =>
{
var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null);
if (cnt != 1) return cnt.ToString();
try
{
var mp = ms.MusicPlayers.FirstOrDefault();
return mp.Value.CurrentSong.SongInfo.Title;
}
catch
{
return "No songs";
}
});
_reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString());
return this;
}
//public ReplacementBuilder WithMusic(MusicService ms)
//{
// _reps.TryAdd("%playing%", () =>
// {
// var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null);
// if (cnt != 1) return cnt.ToString();
// try
// {
// var mp = ms.MusicPlayers.FirstOrDefault();
// return mp.Value.CurrentSong.SongInfo.Title;
// }
// catch
// {
// return "No songs";
// }
// });
// _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString());
// return this;
//}
public ReplacementBuilder WithRngRegex()
{

View File

@ -1,6 +1,7 @@
using NadekoBot.Extensions;
using NadekoBot.Services;
using Newtonsoft.Json;
using NLog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -19,9 +20,11 @@ namespace NadekoBot.DataStructures
private readonly ConcurrentDictionary<DapiSearchType, SemaphoreSlim> _locks = new ConcurrentDictionary<DapiSearchType, SemaphoreSlim>();
private readonly SortedSet<ImageCacherObject> _cache;
private readonly Logger _log;
public SearchImageCacher()
{
_log = LogManager.GetCurrentClassLogger();
_rng = new NadekoRandom();
_cache = new SortedSet<ImageCacherObject>();
}
@ -85,7 +88,7 @@ namespace NadekoBot.DataStructures
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();
if (isExplicit)
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 ReplyConfirmLocalized("rar", Format.Bold(user.ToString())).ConfigureAwait(false);
}
catch (Exception ex)
catch (Exception)
{
Console.WriteLine(ex);
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"];
// if (general == null)
// {
// //Console.WriteLine("General is null.");
// return;
// }
// //get build data for this role
@ -309,7 +308,6 @@ namespace NadekoBot.Modules.Searches
// }
// catch (Exception ex)
// {
// //Console.WriteLine(ex);
// await channel.SendMessageAsync("💢 Failed retreiving data for that champion.").ConfigureAwait(false);
// }
// });

View File

@ -31,6 +31,7 @@ using NadekoBot.Extensions;
namespace NadekoBot
{
//todo log when joining or leaving the server
public class NadekoBot
{
private Logger _log;
@ -183,7 +184,7 @@ namespace NadekoBot
#endregion
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);
#region Games
@ -212,8 +213,6 @@ namespace NadekoBot
var pokemonService = new PokemonService();
#endregion
//initialize Services
Services = new NServiceProvider.ServiceProviderBuilder()
.Add<ILocalization>(Localization)
@ -269,7 +268,6 @@ namespace NadekoBot
.Add<NadekoBot>(this)
.Build();
CommandHandler.AddServices(Services);
//setup typereaders
@ -352,7 +350,7 @@ namespace NadekoBot
#if GLOBAL_NADEKO
isPublicNadeko = true;
#endif
//Console.WriteLine(string.Join(", ", CommandService.Commands
//_log.Info(string.Join(", ", CommandService.Commands
// .Distinct(x => x.Name + x.Module.Name)
// .SelectMany(x => x.Aliases)
// .GroupBy(x => x)

View File

@ -1198,7 +1198,7 @@
<value>`{0}drawnew` or `{0}drawnew 5`</value>
</data>
<data name="shuffleplaylist_cmd" xml:space="preserve">
<value>playlistshuffle plsh</value>
<value>shuffle sh plsh</value>
</data>
<data name="shuffleplaylist_desc" xml:space="preserve">
<value>Shuffles the current playlist.</value>
@ -1467,6 +1467,15 @@
<data name="next_usage" xml:space="preserve">
<value>`{0}n` or `{0}n 5`</value>
</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">
<value>stop s</value>
</data>

View File

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

View File

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

View File

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

View File

@ -104,7 +104,6 @@ namespace NadekoBot.Services.Games
{
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)));
try { await usrMsg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { }
_log.Info(returnMsg);

View File

@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl
private readonly IBotCredentials _creds;
private readonly DateTime _started;
public const string BotVersion = "1.52";
public const string BotVersion = "1.53";
public string Author => "Kwoth#2560";
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
{
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() : 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 NadekoBot.Extensions;
using NadekoBot.Services.Database.Models;
using System.Text.RegularExpressions;
using NLog;
using System.IO;
using VideoLibrary;
using System.Net.Http;
using System.Collections.Generic;
using Discord.Commands;
using Discord.WebSocket;
using System.Text.RegularExpressions;
using System.Net.Http;
namespace NadekoBot.Services.Music
{
@ -26,13 +28,15 @@ namespace NadekoBot.Services.Music
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(IGoogleApiService google,
NadekoStrings strings, ILocalization localization, DbService db,
public MusicService(DiscordSocketClient client, IGoogleApiService google,
NadekoStrings strings, ILocalization localization, DbService db,
SoundCloudApiService sc, IBotCredentials creds, IEnumerable<GuildConfig> gcs)
{
_client = client;
_google = google;
_strings = strings;
_localization = localization;
@ -48,28 +52,44 @@ namespace NadekoBot.Services.Music
Directory.CreateDirectory(MusicDataPath);
}
public MusicPlayer GetPlayer(ulong guildId)
public float GetDefaultVolume(ulong guildId)
{
MusicPlayers.TryGetValue(guildId, out var player);
return player;
return _defaultVolumes.GetOrAdd(guildId, (id) =>
{
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) =>
_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)
{
return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume;
}
});
var mp = new MusicPlayer(voiceCh, textCh, vol, _google);
await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false);
}
throw new ArgumentException(nameof(voiceCh));
}
return MusicPlayers.GetOrAdd(guildId, _ =>
{
var vol = GetDefaultVolume(guildId);
var mp = new MusicPlayer(this, _google, voiceCh, textCh, vol);
IUserMessage playingMessage = null;
IUserMessage lastFinishedMessage = null;
mp.OnCompleted += async (s, song) =>
@ -90,31 +110,19 @@ namespace NadekoBot.Services.Music
{
// 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
{
// ignored
}
};
mp.OnStarted += async (player, song) =>
{
try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); }
catch
{
// ignored
}
//try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); }
//catch
//{
// // ignored
//}
var sender = player;
if (sender == null)
return;
@ -123,9 +131,9 @@ namespace NadekoBot.Services.Music
playingMessage?.DeleteAfter(0);
playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithAuthor(eab => eab.WithName(GetText("playing_song")).WithMusicIcon())
.WithDescription(song.PrettyName)
.WithFooter(ef => ef.WithText(song.PrettyInfo)))
.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
@ -133,7 +141,7 @@ namespace NadekoBot.Services.Music
// ignored
}
};
mp.OnPauseChanged += async (paused) =>
mp.OnPauseChanged += async (player, paused) =>
{
try
{
@ -150,195 +158,177 @@ namespace NadekoBot.Services.Music
// ignored
}
};
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;
});
}
public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal)
public MusicPlayer GetPlayerOrDefault(ulong guildId)
{
string GetText(string text, params object[] replacements) =>
_strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements);
if (voiceCh == null || voiceCh.Guild != textCh.Guild)
{
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
}
if (MusicPlayers.TryGetValue(guildId, out var mp))
return mp;
else
return null;
}
public void DestroyPlayer(ulong id)
public async Task TryQueueRelatedSongAsync(string query, ITextChannel txtCh, IVoiceChannel vch)
{
if (MusicPlayers.TryRemove(id, out var mp))
mp.Destroy();
var related = (await _google.GetRelatedVideosAsync(query, 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<Song> ResolveSong(string query, MusicType musicType = MusicType.Normal)
public async Task<SongInfo> ResolveSong(string query, string queuerName, MusicType? musicType = null)
{
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
query.ThrowIfNull(nameof(query));
if (musicType != MusicType.Local && IsRadioLink(query))
SongInfo sinfo = null;
switch (musicType)
{
musicType = MusicType.Radio;
query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query;
case MusicType.YouTube:
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)
{
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) };
}
Title = svideo.FullName,
Provider = "SoundCloud",
Uri = await svideo.StreamLink().ConfigureAwait(false),
ProviderType = MusicType.Soundcloud,
Query = svideo.TrackLink,
AlbumArt = svideo.artwork_url,
QueuerName = queuerName
};
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)
public SongInfo ResolveLocalSong(string query, string queuerName)
{
return new SongInfo
{
_log.Warn($"Failed resolving the link.{ex.Message}");
_log.Warn(ex);
Uri = "\"" + Path.GetFullPath(query) + "\"",
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;
}
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)
{
string file = null;
@ -359,7 +349,7 @@ namespace NadekoBot.Services.Music
//Regex.Match(query)
try
{
var m = Regex.Match(file, "File1=(?<url>.*?)\\n");
var m = plsRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
@ -378,7 +368,7 @@ namespace NadekoBot.Services.Music
*/
try
{
var m = Regex.Match(file, "(?<url>^[^#].*)", RegexOptions.Multiline);
var m = m3uRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
@ -394,7 +384,7 @@ namespace NadekoBot.Services.Music
//<ref href="http://armitunes.com:8000"/>
try
{
var m = Regex.Match(file, "<ref href=\"(?<url>.*?)\"");
var m = asxRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
@ -414,7 +404,7 @@ namespace NadekoBot.Services.Music
*/
try
{
var m = Regex.Match(file, "<location>(?<url>.*?)</location>");
var m = xspfRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
@ -427,14 +417,5 @@ namespace NadekoBot.Services.Music
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 NLog;
using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Extensions;
using System.Net;
using Discord;
using NadekoBot.Services.Database.Models;
using System;
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 class Song
//{
// public SongInfo SongInfo { get; }
// public MusicPlayer MusicPlayer { get; set; }
public class Song
{
public SongInfo SongInfo { get; }
public MusicPlayer MusicPlayer { get; set; }
// private string _queuerName;
// public string QueuerName { get{
// return Format.Sanitize(_queuerName);
// } set { _queuerName = value; } }
private string _queuerName;
public string QueuerName { get{
return Format.Sanitize(_queuerName);
} set { _queuerName = value; } }
// public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
// public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds));
public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds));
// private const int _milliseconds = 20;
// private const int _samplesPerFrame = (48000 / 1000) * _milliseconds;
// private const int _frameBytes = 3840; //16-bit, 2 channels
private const int _milliseconds = 20;
private const int _samplesPerFrame = (48000 / 1000) * _milliseconds;
private const int _frameBytes = 3840; //16-bit, 2 channels
// private ulong BytesSent { get; set; }
private ulong BytesSent { get; set; }
// //pwetty
//pwetty
// public string PrettyProvider =>
// $"{(SongInfo.Provider ?? "???")}";
public string PrettyProvider =>
$"{(SongInfo.Provider ?? "???")}";
// public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime;
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 {
get {
var time = CurrentTime.ToString(@"mm\:ss");
var hrs = (int)CurrentTime.TotalHours;
// if (hrs > 0)
// return hrs + ":" + time;
// else
// return time;
// }
// }
if (hrs > 0)
return hrs + ":" + time;
else
return time;
}
}
// 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;
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;
// }
// }
if (hrs > 0)
return hrs + ":" + time;
return time;
}
}
// public string Thumbnail {
// get {
// 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 {
get {
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 SongUrl {
// 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 string SongUrl {
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 "";
}
}
}
// private readonly Logger _log;
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)
{
SongInfo = songInfo;
_log = LogManager.GetCurrentClassLogger();
}
// var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100);
// var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false);
public Song Clone()
{
var s = new Song(SongInfo)
{
MusicPlayer = MusicPlayer,
QueuerName = QueuerName
};
return s;
}
// try
// {
// var attempt = 0;
public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
{
BytesSent = (ulong) SkipTo * 3840 * 50;
var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString());
// var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy
// 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;
// }
var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100);
var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false);
// if (inStream.BufferingCompleted && count == 1)
// {
// _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 attempt = 0;
// var outStream = voiceClient.CreatePCMStream(AudioApplication.Music);
var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy
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;
}
// int nextTime = Environment.TickCount + _milliseconds;
if (inStream.BufferingCompleted && count == 1)
{
_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);
// byte[] buffer = new byte[_frameBytes];
// while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason
// !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime
// {
// 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");
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;
byte[] buffer = new byte[_frameBytes];
while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason
!(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;
}
// while (MusicPlayer.Paused)
// {
// await Task.Delay(200, cancelToken).ConfigureAwait(false);
// nextTime = Environment.TickCount + _milliseconds;
// }
buffer = AdjustVolume(buffer, MusicPlayer.Volume);
if (read != _frameBytes) continue;
nextTime = unchecked(nextTime + _milliseconds);
int delayMillis = unchecked(nextTime - Environment.TickCount);
if (delayMillis > 0)
await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false);
await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false);
}
}
finally
{
await bufferTask;
inStream.Dispose();
}
}
// buffer = AdjustVolume(buffer, MusicPlayer.Volume);
// if (read != _frameBytes) continue;
// nextTime = unchecked(nextTime + _milliseconds);
// int delayMillis = unchecked(nextTime - Environment.TickCount);
// if (delayMillis > 0)
// await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false);
// await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false);
// }
// }
// finally
// {
// await bufferTask;
// inStream.Dispose();
// }
// }
private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size)
{
while (!inStream.BufferingCompleted && inStream.Length < size)
{
await Task.Delay(100, cancelToken);
}
_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;
}
}
// private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size)
// {
// while (!inStream.BufferingCompleted && inStream.Length < size)
// {
// await Task.Delay(100, cancelToken);
// }
// _log.Debug("Buffering successfull");
// }
//}
}

View File

@ -1,4 +1,4 @@
using NadekoBot.Extensions;
using NadekoBot.DataStructures;
using NLog;
using System;
using System.Diagnostics;
@ -8,212 +8,378 @@ using System.Threading.Tasks;
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 class SongBuffer : IDisposable
{
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();
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; }
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.
//_log.Info(_outStream.Length);
await Task.Delay(10);
}
//if (cancelToken.IsCancellationRequested)
// _log.Info("Song canceled");
//else if (p.HasExited)
// _log.Info("Song buffered completely (FFmpeg exited)");
//else if (bytesRead == 0)
// _log.Info("Nothing read");
//}
//while (restart && !cancelToken.IsCancellationRequested);
}
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");
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 { }
}
NextFileToRead++;
return filename;
}
private bool IsNextFileReady()
{
return NextFileToRead <= FileNumber;
}
private void CleanFiles()
{
for (long i = NextFileToRead - 1; i <= FileNumber; i++)
{
try
catch (OperationCanceledException) { }
catch (InvalidOperationException) { } // when ffmpeg is disposed
catch (Exception ex)
{
File.Delete(Basename + "-" + i);
_log.Info(ex);
}
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())
finally
{
CurrentFileStream.Dispose();
CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
read += CurrentFileStream.Read(buffer, read + offset, count - read);
if (toReturn.TrySetResult(false))
_log.Info("Prebuffering failed");
}
if (read < count)
Array.Clear(buffer, read, count - read);
}, cancelToken);
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;
}
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();
catch (Exception ex)
{
_log.Error(ex);
}
try
{
if(!this.p.HasExited)
this.p.Kill();
}
catch
{
}
_outStream.Dispose();
this.p.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_autoplay_disabled": "Autoplay disabled.",
"music_autoplay_enabled": "Autoplay enabled.",
"music_autoplaying": "Auto-playing.",
"music_defvol_set": "Default volume set to {0}%",
"music_dir_queue_complete": "Directory queue complete.",
"music_fairplay": "fairplay",
"music_fairplay": "Fairplay",
"music_finished_song": "Finished song",
"music_fp_disabled": "Fair play disabled.",
"music_fp_enabled": "Fair play enabled.",
@ -424,7 +425,7 @@
"music_no_search_results": "No search results.",
"music_paused": "Music playback paused.",
"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_page": "Page {0} of saved playlists",
"music_playlist_deleted": "Playlist deleted.",
@ -437,19 +438,24 @@
"music_queued_song": "Queued song",
"music_queue_cleared": "Music queue cleared.",
"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_error": "Song on that index doesn't exist",
"music_repeating_cur_song": "Repeating current song",
"music_repeating_playlist": "Repeating playlist",
"music_repeating_track": "Repeating track",
"music_repeating_track_stopped": "Current track repeat stopped.",
"music_shuffling_playlist": "Shuffling songs",
"music_resumed": "Music playback resumed.",
"music_rpl_disabled": "Repeat playlist disabled.",
"music_rpl_enabled": "Repeat playlist enabled.",
"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_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_not_found": "No song found.",
"music_song_skips_after": "Songs will skip after {0}",
"music_time_format": "{0}h {1}m {2}s",
"music_to_position": "To position",
"music_unlimited": "unlimited",