commit
507c9de136
116
src/NadekoBot/DataStructures/PoopyRingBuffer.cs
Normal file
116
src/NadekoBot/DataStructures/PoopyRingBuffer.cs
Normal 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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -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;
|
||||
|
23
src/NadekoBot/DataStructures/SyncPrecondition.cs
Normal file
23
src/NadekoBot/DataStructures/SyncPrecondition.cs
Normal 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
|
||||
//}
|
||||
}
|
@ -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
@ -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);
|
||||
// }
|
||||
// });
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -21,6 +21,9 @@ namespace NadekoBot.Services.Administration
|
||||
TimeZoneInfo tz;
|
||||
try
|
||||
{
|
||||
if (x.TimeZoneId == null)
|
||||
tz = null;
|
||||
else
|
||||
tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId);
|
||||
}
|
||||
catch
|
||||
|
@ -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) =>
|
||||
|
@ -12,7 +12,7 @@
|
||||
public enum MusicType
|
||||
{
|
||||
Radio,
|
||||
Normal,
|
||||
YouTube,
|
||||
Local,
|
||||
Soundcloud
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
13
src/NadekoBot/Services/Impl/SyncPreconditionService.cs
Normal file
13
src/NadekoBot/Services/Impl/SyncPreconditionService.cs
Normal 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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -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.") { }
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
593
src/NadekoBot/Services/Music/MusicPlayer.cs
Normal file
593
src/NadekoBot/Services/Music/MusicPlayer.cs
Normal 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));
|
||||
}
|
||||
}
|
167
src/NadekoBot/Services/Music/MusicQueue.cs
Normal file
167
src/NadekoBot/Services/Music/MusicQueue.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
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;
|
||||
}
|
||||
|
||||
public 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 =>
|
||||
{
|
||||
var vol = _defaultVolumes.GetOrAdd(guildId, (id) =>
|
||||
return _defaultVolumes.GetOrAdd(guildId, (id) =>
|
||||
{
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Task<MusicPlayer> GetOrCreatePlayer(ICommandContext context)
|
||||
{
|
||||
var gUsr = (IGuildUser)context.User;
|
||||
var txtCh = (ITextChannel)context.Channel;
|
||||
var vCh = gUsr.VoiceChannel;
|
||||
return GetOrCreatePlayer(context.Guild.Id, vCh, txtCh);
|
||||
}
|
||||
|
||||
public async Task<MusicPlayer> GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh)
|
||||
{
|
||||
string GetText(string text, params object[] replacements) =>
|
||||
_strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements);
|
||||
|
||||
if (voiceCh == null || voiceCh.Guild != textCh.Guild)
|
||||
{
|
||||
if (textCh != null)
|
||||
{
|
||||
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);
|
||||
|
||||
var mp = new MusicPlayer(voiceCh, textCh, vol, _google);
|
||||
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
|
||||
{
|
||||
@ -151,162 +159,127 @@ 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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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 (MusicPlayers.TryGetValue(guildId, out var mp))
|
||||
return mp;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
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
|
||||
public async Task TryQueueRelatedSongAsync(string query, ITextChannel txtCh, IVoiceChannel vch)
|
||||
{
|
||||
musicPlayer.ThrowIfQueueFull();
|
||||
resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false);
|
||||
var related = (await _google.GetRelatedVideosAsync(query, 4)).ToArray();
|
||||
if (!related.Any())
|
||||
return;
|
||||
|
||||
if (resolvedSong == null)
|
||||
var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.YouTube);
|
||||
if (si == 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
|
||||
}
|
||||
var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh);
|
||||
mp.Enqueue(si);
|
||||
}
|
||||
|
||||
public void DestroyPlayer(ulong id)
|
||||
public async Task<SongInfo> ResolveSong(string query, string queuerName, MusicType? musicType = null)
|
||||
{
|
||||
if (MusicPlayers.TryRemove(id, out var mp))
|
||||
mp.Destroy();
|
||||
}
|
||||
query.ThrowIfNull(nameof(query));
|
||||
|
||||
|
||||
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
|
||||
{
|
||||
SongInfo sinfo = null;
|
||||
switch (musicType)
|
||||
{
|
||||
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:
|
||||
return new Song(new SongInfo
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
Title = svideo.FullName,
|
||||
Provider = "SoundCloud",
|
||||
Uri = await svideo.StreamLink().ConfigureAwait(false),
|
||||
ProviderType = MusicType.Soundcloud,
|
||||
Query = svideo.TrackLink,
|
||||
AlbumArt = svideo.artwork_url,
|
||||
QueuerName = queuerName
|
||||
};
|
||||
|
||||
public SongInfo ResolveLocalSong(string query, string queuerName)
|
||||
{
|
||||
return new SongInfo
|
||||
{
|
||||
Uri = "\"" + Path.GetFullPath(query) + "\"",
|
||||
Title = Path.GetFileNameWithoutExtension(query),
|
||||
Provider = "Local File",
|
||||
ProviderType = musicType,
|
||||
ProviderType = MusicType.Local,
|
||||
Query = query,
|
||||
});
|
||||
case MusicType.Radio:
|
||||
return new Song(new SongInfo
|
||||
QueuerName = queuerName
|
||||
};
|
||||
}
|
||||
|
||||
public SongInfo ResolveRadioSong(string query, string queuerName)
|
||||
{
|
||||
return new SongInfo
|
||||
{
|
||||
Uri = query,
|
||||
Title = $"{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) };
|
||||
ProviderType = MusicType.Radio,
|
||||
Query = query,
|
||||
QueuerName = queuerName
|
||||
};
|
||||
}
|
||||
|
||||
if (musicType == MusicType.Soundcloud)
|
||||
public async Task<SongInfo> ResolveYoutubeSong(string query, string queuerName)
|
||||
{
|
||||
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.");
|
||||
{
|
||||
_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
|
||||
@ -315,30 +288,47 @@ namespace NadekoBot.Services.Music
|
||||
.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
|
||||
{
|
||||
_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,
|
||||
});
|
||||
song.SkipTo = gotoTime;
|
||||
ProviderType = MusicType.YouTube,
|
||||
QueuerName = queuerName
|
||||
};
|
||||
return song;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
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)
|
||||
{
|
||||
_log.Warn($"Failed resolving the link.{ex.Message}");
|
||||
_log.Warn(ex);
|
||||
return null;
|
||||
}
|
||||
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"));
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
||||
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));
|
||||
|
||||
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; }
|
||||
|
||||
//pwetty
|
||||
|
||||
public string PrettyProvider =>
|
||||
$"{(SongInfo.Provider ?? "???")}";
|
||||
|
||||
public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime;
|
||||
|
||||
public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**";
|
||||
|
||||
public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {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;
|
||||
|
||||
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;
|
||||
|
||||
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 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 int SkipTo { get; set; }
|
||||
|
||||
private readonly Logger _log;
|
||||
|
||||
public Song(SongInfo songInfo)
|
||||
{
|
||||
SongInfo = songInfo;
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
}
|
||||
|
||||
public Song Clone()
|
||||
{
|
||||
var s = new Song(SongInfo)
|
||||
{
|
||||
MusicPlayer = MusicPlayer,
|
||||
QueuerName = QueuerName
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
|
||||
{
|
||||
BytesSent = (ulong) SkipTo * 3840 * 50;
|
||||
var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString());
|
||||
|
||||
var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100);
|
||||
var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var attempt = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var outStream = voiceClient.CreatePCMStream(AudioApplication.Music);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
//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; } }
|
||||
|
||||
// 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 ulong BytesSent { get; set; }
|
||||
|
||||
// //pwetty
|
||||
|
||||
// public string PrettyProvider =>
|
||||
// $"{(SongInfo.Provider ?? "???")}";
|
||||
|
||||
// public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime;
|
||||
|
||||
// public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**";
|
||||
|
||||
// public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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 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 Song(SongInfo songInfo)
|
||||
// {
|
||||
// SongInfo = songInfo;
|
||||
// _log = LogManager.GetCurrentClassLogger();
|
||||
// }
|
||||
|
||||
// public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
|
||||
// {
|
||||
// BytesSent = (ulong) SkipTo * 3840 * 50;
|
||||
// var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString());
|
||||
|
||||
// var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100);
|
||||
// var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false);
|
||||
|
||||
// try
|
||||
// {
|
||||
// var attempt = 0;
|
||||
|
||||
// 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;
|
||||
// }
|
||||
|
||||
// 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);
|
||||
|
||||
// var outStream = voiceClient.CreatePCMStream(AudioApplication.Music);
|
||||
|
||||
// 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
|
||||
// {
|
||||
// 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);
|
||||
// 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");
|
||||
// }
|
||||
//}
|
||||
}
|
@ -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 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 () =>
|
||||
private Process StartFFmpegProcess(string songUri, float skipTo = 0)
|
||||
{
|
||||
Process p = null;
|
||||
FileStream outStream = null;
|
||||
try
|
||||
{
|
||||
p = Process.Start(new ProcessStartInfo
|
||||
return Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "ffmpeg",
|
||||
Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet",
|
||||
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 = false,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
});
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[81920];
|
||||
int currentFileSize = 0;
|
||||
ulong prebufferSize = 100ul.MiB();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
|
||||
while (!p.HasExited) //Also fix low bandwidth
|
||||
private readonly object locker = new object();
|
||||
public Task<bool> StartBuffering(CancellationToken cancelToken)
|
||||
{
|
||||
int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false);
|
||||
if (currentFileSize >= MaxFileSize)
|
||||
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
|
||||
{
|
||||
outStream.Dispose();
|
||||
}
|
||||
catch { }
|
||||
outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
|
||||
currentFileSize = bytesRead;
|
||||
}
|
||||
else
|
||||
//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)
|
||||
{
|
||||
currentFileSize += bytesRead;
|
||||
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);
|
||||
}
|
||||
CurrentBufferSize += Convert.ToUInt64(bytesRead);
|
||||
await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
|
||||
while (CurrentBufferSize > prebufferSize)
|
||||
await Task.Delay(100, 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"));
|
||||
|
||||
//_log.Info(_outStream.Length);
|
||||
await Task.Delay(10);
|
||||
}
|
||||
BufferingCompleted = true;
|
||||
//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)
|
||||
{
|
||||
var oldclr = Console.ForegroundColor;
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(@"You have not properly installed or configured FFMPEG.
|
||||
_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 (OperationCanceledException) { }
|
||||
catch (InvalidOperationException) { } // when ffmpeg is disposed
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Buffering stopped: {ex.Message}");
|
||||
_log.Info(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (outStream != null)
|
||||
outStream.Dispose();
|
||||
Console.WriteLine($"Buffering done.");
|
||||
if (p != null)
|
||||
if (toReturn.TrySetResult(false))
|
||||
_log.Info("Prebuffering failed");
|
||||
}
|
||||
}, 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
|
||||
{
|
||||
p.Kill();
|
||||
this.p.StandardOutput.Dispose();
|
||||
}
|
||||
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)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(ex);
|
||||
}
|
||||
try
|
||||
{
|
||||
CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length);
|
||||
File.Delete(Basename + "-" + (NextFileToRead - 1));
|
||||
if(!this.p.HasExited)
|
||||
this.p.Kill();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
NextFileToRead++;
|
||||
return filename;
|
||||
}
|
||||
|
||||
private bool IsNextFileReady()
|
||||
catch
|
||||
{
|
||||
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();
|
||||
_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();
|
||||
// }
|
||||
// }
|
||||
//}
|
98
src/NadekoBot/Services/Music/SongInfo.cs
Normal file
98
src/NadekoBot/Services/Music/SongInfo.cs
Normal 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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
src/NadekoBot/Services/Music/SongResolver.cs
Normal file
111
src/NadekoBot/Services/Music/SongResolver.cs
Normal 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;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user