Huge amount of work on the music rework. Around 60% done. Fixed bot getting stuck when server region is changed.
This commit is contained in:
parent
8f5c63a057
commit
d242952d4a
@ -74,25 +74,25 @@ namespace NadekoBot.DataStructures.Replacements
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReplacementBuilder WithMusic(MusicService ms)
|
//public ReplacementBuilder WithMusic(MusicService ms)
|
||||||
{
|
//{
|
||||||
_reps.TryAdd("%playing%", () =>
|
// _reps.TryAdd("%playing%", () =>
|
||||||
{
|
// {
|
||||||
var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null);
|
// var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null);
|
||||||
if (cnt != 1) return cnt.ToString();
|
// if (cnt != 1) return cnt.ToString();
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
var mp = ms.MusicPlayers.FirstOrDefault();
|
// var mp = ms.MusicPlayers.FirstOrDefault();
|
||||||
return mp.Value.CurrentSong.SongInfo.Title;
|
// return mp.Value.CurrentSong.SongInfo.Title;
|
||||||
}
|
// }
|
||||||
catch
|
// catch
|
||||||
{
|
// {
|
||||||
return "No songs";
|
// return "No songs";
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
_reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString());
|
// _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString());
|
||||||
return this;
|
// return this;
|
||||||
}
|
//}
|
||||||
|
|
||||||
public ReplacementBuilder WithRngRegex()
|
public ReplacementBuilder WithRngRegex()
|
||||||
{
|
{
|
||||||
|
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
|
||||||
|
//}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -35,7 +35,7 @@ namespace NadekoBot.Services.Administration
|
|||||||
_rep = new ReplacementBuilder()
|
_rep = new ReplacementBuilder()
|
||||||
.WithClient(client)
|
.WithClient(client)
|
||||||
.WithStats(client)
|
.WithStats(client)
|
||||||
.WithMusic(music)
|
//.WithMusic(music)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
_t = new Timer(async (objState) =>
|
_t = new Timer(async (objState) =>
|
||||||
|
@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl
|
|||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCredentials _creds;
|
||||||
private readonly DateTime _started;
|
private readonly DateTime _started;
|
||||||
|
|
||||||
public const string BotVersion = "1.52";
|
public const string BotVersion = "1.53";
|
||||||
|
|
||||||
public string Author => "Kwoth#2560";
|
public string Author => "Kwoth#2560";
|
||||||
public string Library => "Discord.Net";
|
public string Library => "Discord.Net";
|
||||||
|
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,7 +2,7 @@
|
|||||||
|
|
||||||
namespace NadekoBot.Services.Music
|
namespace NadekoBot.Services.Music
|
||||||
{
|
{
|
||||||
class PlaylistFullException : Exception
|
public class PlaylistFullException : Exception
|
||||||
{
|
{
|
||||||
public PlaylistFullException(string message) : base(message)
|
public PlaylistFullException(string message) : base(message)
|
||||||
{
|
{
|
||||||
@ -10,11 +10,19 @@ namespace NadekoBot.Services.Music
|
|||||||
public PlaylistFullException() : base("Queue is full.") { }
|
public PlaylistFullException() : base("Queue is full.") { }
|
||||||
}
|
}
|
||||||
|
|
||||||
class SongNotFoundException : Exception
|
public class SongNotFoundException : Exception
|
||||||
{
|
{
|
||||||
public SongNotFoundException(string message) : base(message)
|
public SongNotFoundException(string message) : base(message)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
public SongNotFoundException() : base("Song is not found.") { }
|
public SongNotFoundException() : base("Song is not found.") { }
|
||||||
}
|
}
|
||||||
|
public class NotInVoiceChannelException : Exception
|
||||||
|
{
|
||||||
|
public NotInVoiceChannelException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
657
src/NadekoBot/Services/Music/MusicPlayer.cs
Normal file
657
src/NadekoBot/Services/Music/MusicPlayer.cs
Normal file
@ -0,0 +1,657 @@
|
|||||||
|
using Discord;
|
||||||
|
using Discord.Audio;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NLog;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace NadekoBot.Services.Music
|
||||||
|
{
|
||||||
|
public enum StreamState
|
||||||
|
{
|
||||||
|
Resolving,
|
||||||
|
Queued,
|
||||||
|
Playing,
|
||||||
|
Completed
|
||||||
|
}
|
||||||
|
public class MusicPlayer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Task _player;
|
||||||
|
private readonly IVoiceChannel VoiceChannel;
|
||||||
|
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 string PrettyVolume => $"🔉 {(int)(Volume * 100)}%";
|
||||||
|
private TaskCompletionSource<bool> pauseTaskSource { get; set; } = null;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
private IAudioClient _audioClient;
|
||||||
|
private readonly object locker = new object();
|
||||||
|
|
||||||
|
#region events
|
||||||
|
public event Action<MusicPlayer, SongInfo> OnStarted;
|
||||||
|
public event Action<MusicPlayer, SongInfo> OnCompleted;
|
||||||
|
public event Action<MusicPlayer, bool> OnPauseChanged;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume)
|
||||||
|
{
|
||||||
|
_log = LogManager.GetCurrentClassLogger();
|
||||||
|
this.Volume = volume;
|
||||||
|
this.VoiceChannel = vch;
|
||||||
|
this.SongCancelSource = new CancellationTokenSource();
|
||||||
|
this.OutputTextChannel = output;
|
||||||
|
|
||||||
|
_player = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!Exited)
|
||||||
|
{
|
||||||
|
CancellationToken cancelToken;
|
||||||
|
(int Index, SongInfo Song) data;
|
||||||
|
lock (locker)
|
||||||
|
{
|
||||||
|
data = Queue.Current;
|
||||||
|
cancelToken = SongCancelSource.Token;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_log.Info("Checking for songs");
|
||||||
|
if (data.Song == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_log.Info("Connecting");
|
||||||
|
|
||||||
|
|
||||||
|
_log.Info("Starting");
|
||||||
|
var p = Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "ffmpeg",
|
||||||
|
Arguments = $"-i {data.Song.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
});
|
||||||
|
var ac = await GetAudioClient();
|
||||||
|
if (ac == null)
|
||||||
|
continue;
|
||||||
|
var pcm = ac.CreatePCMStream(AudioApplication.Music);
|
||||||
|
|
||||||
|
OnStarted?.Invoke(this, data.Song);
|
||||||
|
|
||||||
|
byte[] buffer = new byte[3840];
|
||||||
|
int bytesRead = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while ((bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0)
|
||||||
|
{
|
||||||
|
var vol = Volume;
|
||||||
|
if (vol != 1)
|
||||||
|
AdjustVolume(buffer, vol);
|
||||||
|
await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken);
|
||||||
|
|
||||||
|
await (pauseTaskSource?.Task ?? Task.CompletedTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_log.Info("Song Canceled");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Warn(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
//flush is known to get stuck from time to time, just cancel it 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_log.Info("Next song");
|
||||||
|
do
|
||||||
|
{
|
||||||
|
await Task.Delay(100);
|
||||||
|
}
|
||||||
|
while (Stopped && !Exited);
|
||||||
|
if(!RepeatCurrentSong)
|
||||||
|
Queue.Next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, SongCancelSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IAudioClient> GetAudioClient(bool reconnect = false)
|
||||||
|
{
|
||||||
|
if (_audioClient == null ||
|
||||||
|
_audioClient.ConnectionState != ConnectionState.Connected ||
|
||||||
|
reconnect)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_audioClient = await VoiceChannel.ConnectAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return _audioClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (bool Success, int Index) Enqueue(SongInfo song)
|
||||||
|
{
|
||||||
|
_log.Info("Adding song");
|
||||||
|
Queue.Add(song);
|
||||||
|
return (true, Queue.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Next()
|
||||||
|
{
|
||||||
|
lock (locker)
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
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()
|
||||||
|
=> 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 void Dispose()
|
||||||
|
{
|
||||||
|
_log.Info("Disposing");
|
||||||
|
lock (locker)
|
||||||
|
{
|
||||||
|
Exited = true;
|
||||||
|
Unpause();
|
||||||
|
}
|
||||||
|
CancelCurrentSong();
|
||||||
|
OnCompleted = null;
|
||||||
|
OnPauseChanged = null;
|
||||||
|
OnStarted = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//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();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
119
src/NadekoBot/Services/Music/MusicQueue.cs
Normal file
119
src/NadekoBot/Services/Music/MusicQueue.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
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; } = new LinkedList<SongInfo>();
|
||||||
|
private int _currentIndex = 0;
|
||||||
|
private 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(SongInfo song)
|
||||||
|
{
|
||||||
|
lock (locker)
|
||||||
|
{
|
||||||
|
Songs.AddLast(song);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Next()
|
||||||
|
{
|
||||||
|
CurrentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, SongInfo[]) ToArray()
|
||||||
|
{
|
||||||
|
lock (locker)
|
||||||
|
{
|
||||||
|
return (CurrentIndex, Songs.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetCurrent()
|
||||||
|
{
|
||||||
|
CurrentIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ using System.IO;
|
|||||||
using VideoLibrary;
|
using VideoLibrary;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Discord.Commands;
|
||||||
|
|
||||||
namespace NadekoBot.Services.Music
|
namespace NadekoBot.Services.Music
|
||||||
{
|
{
|
||||||
@ -48,28 +49,49 @@ namespace NadekoBot.Services.Music
|
|||||||
Directory.CreateDirectory(MusicDataPath);
|
Directory.CreateDirectory(MusicDataPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MusicPlayer GetPlayer(ulong guildId)
|
// public MusicPlayer GetPlayer(ulong guildId)
|
||||||
|
// {
|
||||||
|
// MusicPlayers.TryGetValue(guildId, out var player);
|
||||||
|
// return player;
|
||||||
|
// }
|
||||||
|
public float GetDefaultVolume(ulong guildId)
|
||||||
{
|
{
|
||||||
MusicPlayers.TryGetValue(guildId, out var player);
|
return _defaultVolumes.GetOrAdd(guildId, (id) =>
|
||||||
return player;
|
{
|
||||||
|
using (var uow = _db.UnitOfWork)
|
||||||
|
{
|
||||||
|
return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public MusicPlayer GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh)
|
public Task<MusicPlayer> GetOrCreatePlayer(ICommandContext context)
|
||||||
|
{
|
||||||
|
var gUsr = (IGuildUser)context.User;
|
||||||
|
var txtCh = (ITextChannel)context.Channel;
|
||||||
|
var vCh = gUsr.VoiceChannel;
|
||||||
|
return GetOrCreatePlayer(context.Guild.Id, vCh, txtCh);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MusicPlayer> GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh)
|
||||||
{
|
{
|
||||||
string GetText(string text, params object[] replacements) =>
|
string GetText(string text, params object[] replacements) =>
|
||||||
_strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements);
|
_strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements);
|
||||||
|
|
||||||
return MusicPlayers.GetOrAdd(guildId, server =>
|
if (voiceCh == null || voiceCh.Guild != textCh.Guild)
|
||||||
{
|
{
|
||||||
var vol = _defaultVolumes.GetOrAdd(guildId, (id) =>
|
if (textCh != null)
|
||||||
{
|
{
|
||||||
using (var uow = _db.UnitOfWork)
|
await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false);
|
||||||
{
|
}
|
||||||
return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume;
|
throw new ArgumentException(nameof(voiceCh));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return MusicPlayers.GetOrAdd(guildId, _ =>
|
||||||
|
{
|
||||||
|
var vol = GetDefaultVolume(guildId);
|
||||||
|
var mp = new MusicPlayer(this, voiceCh, textCh, vol);
|
||||||
|
|
||||||
var mp = new MusicPlayer(voiceCh, textCh, vol, _google);
|
|
||||||
IUserMessage playingMessage = null;
|
IUserMessage playingMessage = null;
|
||||||
IUserMessage lastFinishedMessage = null;
|
IUserMessage lastFinishedMessage = null;
|
||||||
mp.OnCompleted += async (s, song) =>
|
mp.OnCompleted += async (s, song) =>
|
||||||
@ -91,30 +113,30 @@ namespace NadekoBot.Services.Music
|
|||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.ProviderType == MusicType.Normal)
|
//todo autoplay should be independent from event handlers
|
||||||
{
|
//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)
|
// var relatedVideos = (await _google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList();
|
||||||
await QueueSong(await textCh.Guild.GetCurrentUserAsync(),
|
// if (relatedVideos.Count > 0)
|
||||||
textCh,
|
// await QueueSong(await textCh.Guild.GetCurrentUserAsync(),
|
||||||
voiceCh,
|
// textCh,
|
||||||
relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)],
|
// voiceCh,
|
||||||
true).ConfigureAwait(false);
|
// relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)],
|
||||||
}
|
// true).ConfigureAwait(false);
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mp.OnStarted += async (player, song) =>
|
mp.OnStarted += async (player, song) =>
|
||||||
{
|
{
|
||||||
try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); }
|
//try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); }
|
||||||
catch
|
//catch
|
||||||
{
|
//{
|
||||||
// ignored
|
// // ignored
|
||||||
}
|
//}
|
||||||
var sender = player;
|
var sender = player;
|
||||||
if (sender == null)
|
if (sender == null)
|
||||||
return;
|
return;
|
||||||
@ -125,7 +147,7 @@ namespace NadekoBot.Services.Music
|
|||||||
playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||||
.WithAuthor(eab => eab.WithName(GetText("playing_song")).WithMusicIcon())
|
.WithAuthor(eab => eab.WithName(GetText("playing_song")).WithMusicIcon())
|
||||||
.WithDescription(song.PrettyName)
|
.WithDescription(song.PrettyName)
|
||||||
.WithFooter(ef => ef.WithText(song.PrettyInfo)))
|
.WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.PrettyInfo)))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@ -133,7 +155,7 @@ namespace NadekoBot.Services.Music
|
|||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
mp.OnPauseChanged += async (paused) =>
|
mp.OnPauseChanged += async (player, paused) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -150,291 +172,228 @@ namespace NadekoBot.Services.Music
|
|||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
//mp.SongRemoved += async (song, index) =>
|
||||||
|
//{
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var embed = new EmbedBuilder()
|
||||||
|
// .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon())
|
||||||
|
// .WithDescription(song.PrettyName)
|
||||||
|
// .WithFooter(ef => ef.WithText(song.PrettyInfo))
|
||||||
|
// .WithErrorColor();
|
||||||
|
|
||||||
mp.SongRemoved += async (song, index) =>
|
// await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false);
|
||||||
{
|
|
||||||
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
|
||||||
}
|
// {
|
||||||
catch
|
// // ignored
|
||||||
{
|
// }
|
||||||
// ignored
|
//};
|
||||||
}
|
|
||||||
};
|
|
||||||
return mp;
|
return mp;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<SongInfo> ResolveSong(string query, string queuerName, MusicType musicType = MusicType.Normal)
|
||||||
public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal)
|
|
||||||
{
|
{
|
||||||
string GetText(string text, params object[] replacements) =>
|
query.ThrowIfNull(nameof(query));
|
||||||
_strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements);
|
|
||||||
|
|
||||||
if (voiceCh == null || voiceCh.Guild != textCh.Guild)
|
SongInfo sinfo;
|
||||||
{
|
|
||||||
if (!silent)
|
|
||||||
await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false);
|
|
||||||
throw new ArgumentNullException(nameof(voiceCh));
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(query) || query.Length < 3)
|
|
||||||
throw new ArgumentException("Invalid song query.", nameof(query));
|
|
||||||
|
|
||||||
var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh);
|
sinfo = await ResolveYoutubeSong(query, queuerName).ConfigureAwait(false);
|
||||||
Song resolvedSong;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
musicPlayer.ThrowIfQueueFull();
|
|
||||||
resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (resolvedSong == null)
|
return sinfo;
|
||||||
throw new SongNotFoundException();
|
}
|
||||||
|
|
||||||
musicPlayer.AddSong(resolvedSong, queuer.Username);
|
public async Task<SongInfo> ResolveYoutubeSong(string query, string queuerName)
|
||||||
}
|
{
|
||||||
catch (PlaylistFullException)
|
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 SongInfo
|
||||||
{
|
{
|
||||||
try
|
Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube"
|
||||||
{
|
Provider = "YouTube",
|
||||||
await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize));
|
Uri = await video.GetUriAsync().ConfigureAwait(false),
|
||||||
}
|
Query = link,
|
||||||
catch
|
ProviderType = MusicType.Normal,
|
||||||
{
|
QueuerName = queuerName
|
||||||
// ignored
|
};
|
||||||
}
|
return song;
|
||||||
throw;
|
|
||||||
}
|
|
||||||
if (!silent)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//var queuedMessage = await textCh.SendConfirmAsync($"🎵 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false);
|
|
||||||
var queuedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor()
|
|
||||||
.WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (musicPlayer.Playlist.Count + 1)).WithMusicIcon())
|
|
||||||
.WithDescription($"{resolvedSong.PrettyName}\n{GetText("queue")} ")
|
|
||||||
.WithThumbnailUrl(resolvedSong.Thumbnail)
|
|
||||||
.WithFooter(ef => ef.WithText(resolvedSong.PrettyProvider)))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
queuedMessage?.DeleteAfter(10);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
} // if queued message sending fails, don't attempt to delete it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DestroyPlayer(ulong id)
|
public void DestroyPlayer(ulong id)
|
||||||
{
|
{
|
||||||
if (MusicPlayers.TryRemove(id, out var mp))
|
if (MusicPlayers.TryRemove(id, out var mp))
|
||||||
mp.Destroy();
|
mp.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal)
|
||||||
|
// {
|
||||||
|
// string GetText(string text, params object[] replacements) =>
|
||||||
|
// _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements);
|
||||||
|
|
||||||
public async Task<Song> ResolveSong(string query, MusicType musicType = MusicType.Normal)
|
//if (string.IsNullOrWhiteSpace(query) || query.Length< 3)
|
||||||
{
|
// throw new ArgumentException("Invalid song query.", nameof(query));
|
||||||
if (string.IsNullOrWhiteSpace(query))
|
|
||||||
throw new ArgumentNullException(nameof(query));
|
|
||||||
|
|
||||||
if (musicType != MusicType.Local && IsRadioLink(query))
|
// var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh);
|
||||||
{
|
// Song resolvedSong;
|
||||||
musicType = MusicType.Radio;
|
// try
|
||||||
query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query;
|
// {
|
||||||
}
|
// musicPlayer.ThrowIfQueueFull();
|
||||||
|
// resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
// if (resolvedSong == null)
|
||||||
{
|
// throw new SongNotFoundException();
|
||||||
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)
|
// musicPlayer.AddSong(resolvedSong, queuer.Username);
|
||||||
{
|
// }
|
||||||
var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false);
|
// catch (PlaylistFullException)
|
||||||
return new Song(new SongInfo
|
// {
|
||||||
{
|
// try
|
||||||
Title = svideo.FullName,
|
// {
|
||||||
Provider = "SoundCloud",
|
// await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize));
|
||||||
Uri = await svideo.StreamLink(),
|
// }
|
||||||
ProviderType = MusicType.Soundcloud,
|
// catch
|
||||||
Query = svideo.TrackLink,
|
// {
|
||||||
AlbumArt = svideo.artwork_url,
|
// // ignored
|
||||||
})
|
// }
|
||||||
{ TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) };
|
// 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 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> HandleStreamContainers(string query)
|
|
||||||
{
|
|
||||||
string file = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var http = new HttpClient())
|
|
||||||
{
|
|
||||||
file = await http.GetStringAsync(query).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
if (query.Contains(".pls"))
|
|
||||||
{
|
|
||||||
//File1=http://armitunes.com:8000/
|
|
||||||
//Regex.Match(query)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var m = Regex.Match(file, "File1=(?<url>.*?)\\n");
|
|
||||||
var res = m.Groups["url"]?.ToString();
|
|
||||||
return res?.Trim();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_log.Warn($"Failed reading .pls:\n{file}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (query.Contains(".m3u"))
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
# This is a comment
|
|
||||||
C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
|
|
||||||
C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
|
|
||||||
*/
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var m = Regex.Match(file, "(?<url>^[^#].*)", RegexOptions.Multiline);
|
|
||||||
var res = m.Groups["url"]?.ToString();
|
|
||||||
return res?.Trim();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_log.Warn($"Failed reading .m3u:\n{file}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if (query.Contains(".asx"))
|
|
||||||
{
|
|
||||||
//<ref href="http://armitunes.com:8000"/>
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var m = Regex.Match(file, "<ref href=\"(?<url>.*?)\"");
|
|
||||||
var res = m.Groups["url"]?.ToString();
|
|
||||||
return res?.Trim();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_log.Warn($"Failed reading .asx:\n{file}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (query.Contains(".xspf"))
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<playlist version="1" xmlns="http://xspf.org/ns/0/">
|
|
||||||
<trackList>
|
|
||||||
<track><location>file:///mp3s/song_1.mp3</location></track>
|
|
||||||
*/
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var m = Regex.Match(file, "<location>(?<url>.*?)</location>");
|
|
||||||
var res = m.Groups["url"]?.ToString();
|
|
||||||
return res?.Trim();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_log.Warn($"Failed reading .xspf:\n{file}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsRadioLink(string query) =>
|
// private async Task<string> HandleStreamContainers(string query)
|
||||||
(query.StartsWith("http") ||
|
// {
|
||||||
query.StartsWith("ww"))
|
// string file = null;
|
||||||
&&
|
// try
|
||||||
(query.Contains(".pls") ||
|
// {
|
||||||
query.Contains(".m3u") ||
|
// using (var http = new HttpClient())
|
||||||
query.Contains(".asx") ||
|
// {
|
||||||
query.Contains(".xspf"));
|
// file = await http.GetStringAsync(query).ConfigureAwait(false);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// catch
|
||||||
|
// {
|
||||||
|
// return query;
|
||||||
|
// }
|
||||||
|
// if (query.Contains(".pls"))
|
||||||
|
// {
|
||||||
|
// //File1=http://armitunes.com:8000/
|
||||||
|
// //Regex.Match(query)
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var m = Regex.Match(file, "File1=(?<url>.*?)\\n");
|
||||||
|
// var res = m.Groups["url"]?.ToString();
|
||||||
|
// return res?.Trim();
|
||||||
|
// }
|
||||||
|
// catch
|
||||||
|
// {
|
||||||
|
// _log.Warn($"Failed reading .pls:\n{file}");
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (query.Contains(".m3u"))
|
||||||
|
// {
|
||||||
|
// /*
|
||||||
|
//# This is a comment
|
||||||
|
// C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
|
||||||
|
// C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
|
||||||
|
// */
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var m = Regex.Match(file, "(?<url>^[^#].*)", RegexOptions.Multiline);
|
||||||
|
// var res = m.Groups["url"]?.ToString();
|
||||||
|
// return res?.Trim();
|
||||||
|
// }
|
||||||
|
// catch
|
||||||
|
// {
|
||||||
|
// _log.Warn($"Failed reading .m3u:\n{file}");
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
// if (query.Contains(".asx"))
|
||||||
|
// {
|
||||||
|
// //<ref href="http://armitunes.com:8000"/>
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var m = Regex.Match(file, "<ref href=\"(?<url>.*?)\"");
|
||||||
|
// var res = m.Groups["url"]?.ToString();
|
||||||
|
// return res?.Trim();
|
||||||
|
// }
|
||||||
|
// catch
|
||||||
|
// {
|
||||||
|
// _log.Warn($"Failed reading .asx:\n{file}");
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (query.Contains(".xspf"))
|
||||||
|
// {
|
||||||
|
// /*
|
||||||
|
// <?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
// <playlist version="1" xmlns="http://xspf.org/ns/0/">
|
||||||
|
// <trackList>
|
||||||
|
// <track><location>file:///mp3s/song_1.mp3</location></track>
|
||||||
|
// */
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var m = Regex.Match(file, "<location>(?<url>.*?)</location>");
|
||||||
|
// var res = m.Groups["url"]?.ToString();
|
||||||
|
// return res?.Trim();
|
||||||
|
// }
|
||||||
|
// catch
|
||||||
|
// {
|
||||||
|
// _log.Warn($"Failed reading .xspf:\n{file}");
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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,246 @@
|
|||||||
using Discord.Audio;
|
using NadekoBot.Extensions;
|
||||||
using NadekoBot.Extensions;
|
|
||||||
using NLog;
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Discord;
|
using Discord;
|
||||||
using NadekoBot.Services.Database.Models;
|
using NadekoBot.Services.Database.Models;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace NadekoBot.Services.Music
|
namespace NadekoBot.Services.Music
|
||||||
{
|
{
|
||||||
public class SongInfo
|
//public class Song
|
||||||
{
|
//{
|
||||||
public string Provider { get; set; }
|
// public SongInfo SongInfo { get; }
|
||||||
public MusicType ProviderType { get; set; }
|
// public MusicPlayer MusicPlayer { get; set; }
|
||||||
public string Query { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Uri { get; set; }
|
|
||||||
public string AlbumArt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Song
|
// private string _queuerName;
|
||||||
{
|
// public string QueuerName { get{
|
||||||
public SongInfo SongInfo { get; }
|
// return Format.Sanitize(_queuerName);
|
||||||
public MusicPlayer MusicPlayer { get; set; }
|
// } set { _queuerName = value; } }
|
||||||
|
|
||||||
private string _queuerName;
|
// public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
|
||||||
public string QueuerName { get{
|
// public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds));
|
||||||
return Format.Sanitize(_queuerName);
|
|
||||||
} set { _queuerName = value; } }
|
|
||||||
|
|
||||||
public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
|
// private const int _milliseconds = 20;
|
||||||
public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds));
|
// private const int _samplesPerFrame = (48000 / 1000) * _milliseconds;
|
||||||
|
// private const int _frameBytes = 3840; //16-bit, 2 channels
|
||||||
|
|
||||||
private const int _milliseconds = 20;
|
// private ulong BytesSent { get; set; }
|
||||||
private const int _samplesPerFrame = (48000 / 1000) * _milliseconds;
|
|
||||||
private const int _frameBytes = 3840; //16-bit, 2 channels
|
|
||||||
|
|
||||||
private ulong BytesSent { get; set; }
|
// //pwetty
|
||||||
|
|
||||||
//pwetty
|
// public string PrettyProvider =>
|
||||||
|
// $"{(SongInfo.Provider ?? "???")}";
|
||||||
|
|
||||||
public string PrettyProvider =>
|
// public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime;
|
||||||
$"{(SongInfo.Provider ?? "???")}";
|
|
||||||
|
|
||||||
public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime;
|
// public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**";
|
||||||
|
|
||||||
public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**";
|
// public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}";
|
||||||
|
|
||||||
public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}";
|
// public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`";
|
||||||
|
|
||||||
public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`";
|
// public string PrettyCurrentTime {
|
||||||
|
// get {
|
||||||
|
// var time = CurrentTime.ToString(@"mm\:ss");
|
||||||
|
// var hrs = (int)CurrentTime.TotalHours;
|
||||||
|
|
||||||
public string PrettyCurrentTime {
|
// if (hrs > 0)
|
||||||
get {
|
// return hrs + ":" + time;
|
||||||
var time = CurrentTime.ToString(@"mm\:ss");
|
// else
|
||||||
var hrs = (int)CurrentTime.TotalHours;
|
// return time;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if (hrs > 0)
|
// public string PrettyTotalTime {
|
||||||
return hrs + ":" + time;
|
// get
|
||||||
else
|
// {
|
||||||
return time;
|
// if (TotalTime == TimeSpan.Zero)
|
||||||
}
|
// return "(?)";
|
||||||
}
|
// if (TotalTime == TimeSpan.MaxValue)
|
||||||
|
// return "∞";
|
||||||
|
// var time = TotalTime.ToString(@"mm\:ss");
|
||||||
|
// var hrs = (int)TotalTime.TotalHours;
|
||||||
|
|
||||||
public string PrettyTotalTime {
|
// if (hrs > 0)
|
||||||
get
|
// return hrs + ":" + time;
|
||||||
{
|
// return time;
|
||||||
if (TotalTime == TimeSpan.Zero)
|
// }
|
||||||
return "(?)";
|
// }
|
||||||
if (TotalTime == TimeSpan.MaxValue)
|
|
||||||
return "∞";
|
|
||||||
var time = TotalTime.ToString(@"mm\:ss");
|
|
||||||
var hrs = (int)TotalTime.TotalHours;
|
|
||||||
|
|
||||||
if (hrs > 0)
|
// public string Thumbnail {
|
||||||
return hrs + ":" + time;
|
// get {
|
||||||
return time;
|
// switch (SongInfo.ProviderType)
|
||||||
}
|
// {
|
||||||
}
|
// case MusicType.Radio:
|
||||||
|
// return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links
|
||||||
|
// case MusicType.Normal:
|
||||||
|
// //todo 50 have videoid in songinfo from the start
|
||||||
|
// var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+");
|
||||||
|
// return $"https://img.youtube.com/vi/{ videoId }/0.jpg";
|
||||||
|
// case MusicType.Local:
|
||||||
|
// return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links
|
||||||
|
// case MusicType.Soundcloud:
|
||||||
|
// return SongInfo.AlbumArt;
|
||||||
|
// default:
|
||||||
|
// return "";
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
public string Thumbnail {
|
// public string SongUrl {
|
||||||
get {
|
// get {
|
||||||
switch (SongInfo.ProviderType)
|
// switch (SongInfo.ProviderType)
|
||||||
{
|
// {
|
||||||
case MusicType.Radio:
|
// case MusicType.Normal:
|
||||||
return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links
|
// return SongInfo.Query;
|
||||||
case MusicType.Normal:
|
// case MusicType.Soundcloud:
|
||||||
//todo 50 have videoid in songinfo from the start
|
// return SongInfo.Query;
|
||||||
var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+");
|
// case MusicType.Local:
|
||||||
return $"https://img.youtube.com/vi/{ videoId }/0.jpg";
|
// return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }";
|
||||||
case MusicType.Local:
|
// case MusicType.Radio:
|
||||||
return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links
|
// return $"https://google.com/search?q={SongInfo.Title}";
|
||||||
case MusicType.Soundcloud:
|
// default:
|
||||||
return SongInfo.AlbumArt;
|
// return "";
|
||||||
default:
|
// }
|
||||||
return "";
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string SongUrl {
|
// private readonly Logger _log;
|
||||||
get {
|
|
||||||
switch (SongInfo.ProviderType)
|
|
||||||
{
|
|
||||||
case MusicType.Normal:
|
|
||||||
return SongInfo.Query;
|
|
||||||
case MusicType.Soundcloud:
|
|
||||||
return SongInfo.Query;
|
|
||||||
case MusicType.Local:
|
|
||||||
return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }";
|
|
||||||
case MusicType.Radio:
|
|
||||||
return $"https://google.com/search?q={SongInfo.Title}";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int SkipTo { get; set; }
|
// public Song(SongInfo songInfo)
|
||||||
|
// {
|
||||||
|
// SongInfo = songInfo;
|
||||||
|
// _log = LogManager.GetCurrentClassLogger();
|
||||||
|
// }
|
||||||
|
|
||||||
private readonly Logger _log;
|
// public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
|
||||||
|
// {
|
||||||
|
// BytesSent = (ulong) SkipTo * 3840 * 50;
|
||||||
|
// var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString());
|
||||||
|
|
||||||
public Song(SongInfo songInfo)
|
// var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100);
|
||||||
{
|
// var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false);
|
||||||
SongInfo = songInfo;
|
|
||||||
_log = LogManager.GetCurrentClassLogger();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Song Clone()
|
// try
|
||||||
{
|
// {
|
||||||
var s = new Song(SongInfo)
|
// var attempt = 0;
|
||||||
{
|
|
||||||
MusicPlayer = MusicPlayer,
|
|
||||||
QueuerName = QueuerName
|
|
||||||
};
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
|
// var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy
|
||||||
{
|
// var finished = false;
|
||||||
BytesSent = (ulong) SkipTo * 3840 * 50;
|
// var count = 0;
|
||||||
var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString());
|
// var sw = new Stopwatch();
|
||||||
|
// var slowconnection = false;
|
||||||
|
// sw.Start();
|
||||||
|
// while (!finished)
|
||||||
|
// {
|
||||||
|
// var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken));
|
||||||
|
// if (t != prebufferingTask)
|
||||||
|
// {
|
||||||
|
// count++;
|
||||||
|
// if (count == 10)
|
||||||
|
// {
|
||||||
|
// slowconnection = true;
|
||||||
|
// prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB());
|
||||||
|
// _log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud");
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100);
|
// if (inStream.BufferingCompleted && count == 1)
|
||||||
var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false);
|
// {
|
||||||
|
// _log.Debug("Prebuffering canceled. Cannot get any data from the stream.");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// else if (prebufferingTask.IsCanceled)
|
||||||
|
// {
|
||||||
|
// _log.Debug("Prebuffering canceled. Cannot get any data from the stream.");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// finished = true;
|
||||||
|
// }
|
||||||
|
// sw.Stop();
|
||||||
|
// _log.Debug("Prebuffering successfully completed in " + sw.Elapsed);
|
||||||
|
|
||||||
try
|
// var outStream = voiceClient.CreatePCMStream(AudioApplication.Music);
|
||||||
{
|
|
||||||
var attempt = 0;
|
|
||||||
|
|
||||||
var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy
|
// int nextTime = Environment.TickCount + _milliseconds;
|
||||||
var finished = false;
|
|
||||||
var count = 0;
|
|
||||||
var sw = new Stopwatch();
|
|
||||||
var slowconnection = false;
|
|
||||||
sw.Start();
|
|
||||||
while (!finished)
|
|
||||||
{
|
|
||||||
var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken));
|
|
||||||
if (t != prebufferingTask)
|
|
||||||
{
|
|
||||||
count++;
|
|
||||||
if (count == 10)
|
|
||||||
{
|
|
||||||
slowconnection = true;
|
|
||||||
prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB());
|
|
||||||
_log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inStream.BufferingCompleted && count == 1)
|
// byte[] buffer = new byte[_frameBytes];
|
||||||
{
|
// while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason
|
||||||
_log.Debug("Prebuffering canceled. Cannot get any data from the stream.");
|
// !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime
|
||||||
return;
|
// {
|
||||||
}
|
// //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------");
|
||||||
else
|
// var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||||
{
|
// //await inStream.CopyToAsync(voiceClient.OutputStream);
|
||||||
continue;
|
// if (read < _frameBytes)
|
||||||
}
|
// _log.Debug("read {0}", read);
|
||||||
}
|
// unchecked
|
||||||
else if (prebufferingTask.IsCanceled)
|
// {
|
||||||
{
|
// BytesSent += (ulong)read;
|
||||||
_log.Debug("Prebuffering canceled. Cannot get any data from the stream.");
|
// }
|
||||||
return;
|
// if (read < _frameBytes)
|
||||||
}
|
// {
|
||||||
finished = true;
|
// if (read == 0)
|
||||||
}
|
// {
|
||||||
sw.Stop();
|
// if (inStream.BufferingCompleted)
|
||||||
_log.Debug("Prebuffering successfully completed in " + sw.Elapsed);
|
// break;
|
||||||
|
// if (attempt++ == 20)
|
||||||
|
// {
|
||||||
|
// MusicPlayer.SongCancelSource.Cancel();
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// if (slowconnection)
|
||||||
|
// {
|
||||||
|
// _log.Warn("Slow connection has disrupted music, waiting a bit for buffer");
|
||||||
|
|
||||||
var outStream = voiceClient.CreatePCMStream(AudioApplication.Music);
|
// await Task.Delay(1000, cancelToken).ConfigureAwait(false);
|
||||||
|
// nextTime = Environment.TickCount + _milliseconds;
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// await Task.Delay(100, cancelToken).ConfigureAwait(false);
|
||||||
|
// nextTime = Environment.TickCount + _milliseconds;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// attempt = 0;
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// attempt = 0;
|
||||||
|
|
||||||
int nextTime = Environment.TickCount + _milliseconds;
|
// while (MusicPlayer.Paused)
|
||||||
|
// {
|
||||||
byte[] buffer = new byte[_frameBytes];
|
// await Task.Delay(200, cancelToken).ConfigureAwait(false);
|
||||||
while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason
|
// nextTime = Environment.TickCount + _milliseconds;
|
||||||
!(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime
|
// }
|
||||||
{
|
|
||||||
//Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------");
|
|
||||||
var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
|
||||||
//await inStream.CopyToAsync(voiceClient.OutputStream);
|
|
||||||
if (read < _frameBytes)
|
|
||||||
_log.Debug("read {0}", read);
|
|
||||||
unchecked
|
|
||||||
{
|
|
||||||
BytesSent += (ulong)read;
|
|
||||||
}
|
|
||||||
if (read < _frameBytes)
|
|
||||||
{
|
|
||||||
if (read == 0)
|
|
||||||
{
|
|
||||||
if (inStream.BufferingCompleted)
|
|
||||||
break;
|
|
||||||
if (attempt++ == 20)
|
|
||||||
{
|
|
||||||
MusicPlayer.SongCancelSource.Cancel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (slowconnection)
|
|
||||||
{
|
|
||||||
_log.Warn("Slow connection has disrupted music, waiting a bit for buffer");
|
|
||||||
|
|
||||||
await Task.Delay(1000, cancelToken).ConfigureAwait(false);
|
|
||||||
nextTime = Environment.TickCount + _milliseconds;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Task.Delay(100, cancelToken).ConfigureAwait(false);
|
|
||||||
nextTime = Environment.TickCount + _milliseconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
attempt = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
attempt = 0;
|
|
||||||
|
|
||||||
while (MusicPlayer.Paused)
|
|
||||||
{
|
|
||||||
await Task.Delay(200, cancelToken).ConfigureAwait(false);
|
|
||||||
nextTime = Environment.TickCount + _milliseconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
buffer = AdjustVolume(buffer, MusicPlayer.Volume);
|
// buffer = AdjustVolume(buffer, MusicPlayer.Volume);
|
||||||
if (read != _frameBytes) continue;
|
// if (read != _frameBytes) continue;
|
||||||
nextTime = unchecked(nextTime + _milliseconds);
|
// nextTime = unchecked(nextTime + _milliseconds);
|
||||||
int delayMillis = unchecked(nextTime - Environment.TickCount);
|
// int delayMillis = unchecked(nextTime - Environment.TickCount);
|
||||||
if (delayMillis > 0)
|
// if (delayMillis > 0)
|
||||||
await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false);
|
// await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false);
|
||||||
await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false);
|
// await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
finally
|
// finally
|
||||||
{
|
// {
|
||||||
await bufferTask;
|
// await bufferTask;
|
||||||
inStream.Dispose();
|
// inStream.Dispose();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size)
|
// private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size)
|
||||||
{
|
// {
|
||||||
while (!inStream.BufferingCompleted && inStream.Length < size)
|
// while (!inStream.BufferingCompleted && inStream.Length < size)
|
||||||
{
|
// {
|
||||||
await Task.Delay(100, cancelToken);
|
// await Task.Delay(100, cancelToken);
|
||||||
}
|
// }
|
||||||
_log.Debug("Buffering successfull");
|
// _log.Debug("Buffering successfull");
|
||||||
}
|
// }
|
||||||
|
//}
|
||||||
//aidiakapi ftw
|
|
||||||
public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume)
|
|
||||||
{
|
|
||||||
if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples;
|
|
||||||
|
|
||||||
// 16-bit precision for the multiplication
|
|
||||||
var volumeFixed = (int)Math.Round(volume * 65536d);
|
|
||||||
|
|
||||||
var count = audioSamples.Length / 2;
|
|
||||||
|
|
||||||
fixed (byte* srcBytes = audioSamples)
|
|
||||||
{
|
|
||||||
var src = (short*)srcBytes;
|
|
||||||
|
|
||||||
for (var i = count; i != 0; i--, src++)
|
|
||||||
*src = (short)(((*src) * volumeFixed) >> 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
return audioSamples;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,219 +1,219 @@
|
|||||||
using NadekoBot.Extensions;
|
//using NadekoBot.Extensions;
|
||||||
using NLog;
|
//using NLog;
|
||||||
using System;
|
//using System;
|
||||||
using System.Diagnostics;
|
//using System.Diagnostics;
|
||||||
using System.IO;
|
//using System.IO;
|
||||||
using System.Threading;
|
//using System.Threading;
|
||||||
using System.Threading.Tasks;
|
//using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace NadekoBot.Services.Music
|
//namespace NadekoBot.Services.Music
|
||||||
{
|
//{
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space.
|
// /// 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.
|
// /// It also help for large music by deleting files that are already seen.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
class SongBuffer : Stream
|
// class SongBuffer : Stream
|
||||||
{
|
// {
|
||||||
public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize)
|
// public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize)
|
||||||
{
|
// {
|
||||||
MusicPlayer = musicPlayer;
|
// MusicPlayer = musicPlayer;
|
||||||
Basename = basename;
|
// Basename = basename;
|
||||||
SongInfo = songInfo;
|
// SongInfo = songInfo;
|
||||||
SkipTo = skipTo;
|
// SkipTo = skipTo;
|
||||||
MaxFileSize = maxFileSize;
|
// MaxFileSize = maxFileSize;
|
||||||
CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
|
// CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
|
||||||
_log = LogManager.GetCurrentClassLogger();
|
// _log = LogManager.GetCurrentClassLogger();
|
||||||
}
|
// }
|
||||||
|
|
||||||
MusicPlayer MusicPlayer { get; }
|
// MusicPlayer MusicPlayer { get; }
|
||||||
|
|
||||||
private string Basename { get; }
|
// private string Basename { get; }
|
||||||
|
|
||||||
private SongInfo SongInfo { get; }
|
// private SongInfo SongInfo { get; }
|
||||||
|
|
||||||
private int SkipTo { get; }
|
// private int SkipTo { get; }
|
||||||
|
|
||||||
private int MaxFileSize { get; } = 2.MiB();
|
// private int MaxFileSize { get; } = 2.MiB();
|
||||||
|
|
||||||
private long FileNumber = -1;
|
// private long FileNumber = -1;
|
||||||
|
|
||||||
private long NextFileToRead = 0;
|
// private long NextFileToRead = 0;
|
||||||
|
|
||||||
public bool BufferingCompleted { get; private set; } = false;
|
// public bool BufferingCompleted { get; private set; } = false;
|
||||||
|
|
||||||
private ulong CurrentBufferSize = 0;
|
// private ulong CurrentBufferSize = 0;
|
||||||
|
|
||||||
private FileStream CurrentFileStream;
|
// private FileStream CurrentFileStream;
|
||||||
private Logger _log;
|
// private Logger _log;
|
||||||
|
|
||||||
public Task BufferSong(CancellationToken cancelToken) =>
|
// public Task BufferSong(CancellationToken cancelToken) =>
|
||||||
Task.Run(async () =>
|
// Task.Run(async () =>
|
||||||
{
|
// {
|
||||||
Process p = null;
|
// Process p = null;
|
||||||
FileStream outStream = null;
|
// FileStream outStream = null;
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
p = Process.Start(new ProcessStartInfo
|
// p = Process.Start(new ProcessStartInfo
|
||||||
{
|
// {
|
||||||
FileName = "ffmpeg",
|
// FileName = "ffmpeg",
|
||||||
Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet",
|
// Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet",
|
||||||
UseShellExecute = false,
|
// UseShellExecute = false,
|
||||||
RedirectStandardOutput = true,
|
// RedirectStandardOutput = true,
|
||||||
RedirectStandardError = false,
|
// RedirectStandardError = false,
|
||||||
CreateNoWindow = true,
|
// CreateNoWindow = true,
|
||||||
});
|
// });
|
||||||
|
|
||||||
byte[] buffer = new byte[81920];
|
// byte[] buffer = new byte[81920];
|
||||||
int currentFileSize = 0;
|
// int currentFileSize = 0;
|
||||||
ulong prebufferSize = 100ul.MiB();
|
// ulong prebufferSize = 100ul.MiB();
|
||||||
|
|
||||||
outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
|
// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
|
||||||
while (!p.HasExited) //Also fix low bandwidth
|
// while (!p.HasExited) //Also fix low bandwidth
|
||||||
{
|
// {
|
||||||
int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false);
|
// int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false);
|
||||||
if (currentFileSize >= MaxFileSize)
|
// if (currentFileSize >= MaxFileSize)
|
||||||
{
|
// {
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
outStream.Dispose();
|
// outStream.Dispose();
|
||||||
}
|
// }
|
||||||
catch { }
|
// catch { }
|
||||||
outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
|
// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
|
||||||
currentFileSize = bytesRead;
|
// currentFileSize = bytesRead;
|
||||||
}
|
// }
|
||||||
else
|
// else
|
||||||
{
|
// {
|
||||||
currentFileSize += bytesRead;
|
// currentFileSize += bytesRead;
|
||||||
}
|
// }
|
||||||
CurrentBufferSize += Convert.ToUInt64(bytesRead);
|
// CurrentBufferSize += Convert.ToUInt64(bytesRead);
|
||||||
await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
|
// await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
|
||||||
while (CurrentBufferSize > prebufferSize)
|
// while (CurrentBufferSize > prebufferSize)
|
||||||
await Task.Delay(100, cancelToken);
|
// await Task.Delay(100, cancelToken);
|
||||||
}
|
// }
|
||||||
BufferingCompleted = true;
|
// BufferingCompleted = true;
|
||||||
}
|
// }
|
||||||
catch (System.ComponentModel.Win32Exception)
|
// catch (System.ComponentModel.Win32Exception)
|
||||||
{
|
// {
|
||||||
var oldclr = Console.ForegroundColor;
|
// var oldclr = Console.ForegroundColor;
|
||||||
Console.ForegroundColor = ConsoleColor.Red;
|
// Console.ForegroundColor = ConsoleColor.Red;
|
||||||
Console.WriteLine(@"You have not properly installed or configured FFMPEG.
|
// Console.WriteLine(@"You have not properly installed or configured FFMPEG.
|
||||||
Please install and configure FFMPEG to play music.
|
//Please install and configure FFMPEG to play music.
|
||||||
Check the guides for your platform on how to setup ffmpeg correctly:
|
//Check the guides for your platform on how to setup ffmpeg correctly:
|
||||||
Windows Guide: https://goo.gl/OjKk8F
|
// Windows Guide: https://goo.gl/OjKk8F
|
||||||
Linux Guide: https://goo.gl/ShjCUo");
|
// Linux Guide: https://goo.gl/ShjCUo");
|
||||||
Console.ForegroundColor = oldclr;
|
// Console.ForegroundColor = oldclr;
|
||||||
}
|
// }
|
||||||
catch (Exception ex)
|
// catch (Exception ex)
|
||||||
{
|
// {
|
||||||
Console.WriteLine($"Buffering stopped: {ex.Message}");
|
// Console.WriteLine($"Buffering stopped: {ex.Message}");
|
||||||
}
|
// }
|
||||||
finally
|
// finally
|
||||||
{
|
// {
|
||||||
if (outStream != null)
|
// if (outStream != null)
|
||||||
outStream.Dispose();
|
// outStream.Dispose();
|
||||||
Console.WriteLine($"Buffering done.");
|
// Console.WriteLine($"Buffering done.");
|
||||||
if (p != null)
|
// if (p != null)
|
||||||
{
|
// {
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
p.Kill();
|
// p.Kill();
|
||||||
}
|
// }
|
||||||
catch { }
|
// catch { }
|
||||||
p.Dispose();
|
// p.Dispose();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Return the next file to read, and delete the old one
|
// /// Return the next file to read, and delete the old one
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
/// <returns>Name of the file to read</returns>
|
// /// <returns>Name of the file to read</returns>
|
||||||
private string GetNextFile()
|
// private string GetNextFile()
|
||||||
{
|
// {
|
||||||
string filename = Basename + "-" + NextFileToRead;
|
// string filename = Basename + "-" + NextFileToRead;
|
||||||
|
|
||||||
if (NextFileToRead != 0)
|
// if (NextFileToRead != 0)
|
||||||
{
|
// {
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length);
|
// CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length);
|
||||||
File.Delete(Basename + "-" + (NextFileToRead - 1));
|
// File.Delete(Basename + "-" + (NextFileToRead - 1));
|
||||||
}
|
// }
|
||||||
catch { }
|
// catch { }
|
||||||
}
|
// }
|
||||||
NextFileToRead++;
|
// NextFileToRead++;
|
||||||
return filename;
|
// return filename;
|
||||||
}
|
// }
|
||||||
|
|
||||||
private bool IsNextFileReady()
|
// private bool IsNextFileReady()
|
||||||
{
|
// {
|
||||||
return NextFileToRead <= FileNumber;
|
// return NextFileToRead <= FileNumber;
|
||||||
}
|
// }
|
||||||
|
|
||||||
private void CleanFiles()
|
// private void CleanFiles()
|
||||||
{
|
// {
|
||||||
for (long i = NextFileToRead - 1; i <= FileNumber; i++)
|
// for (long i = NextFileToRead - 1; i <= FileNumber; i++)
|
||||||
{
|
// {
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
File.Delete(Basename + "-" + i);
|
// File.Delete(Basename + "-" + i);
|
||||||
}
|
// }
|
||||||
catch { }
|
// catch { }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
//Stream part
|
// //Stream part
|
||||||
|
|
||||||
public override bool CanRead => true;
|
// public override bool CanRead => true;
|
||||||
|
|
||||||
public override bool CanSeek => false;
|
// public override bool CanSeek => false;
|
||||||
|
|
||||||
public override bool CanWrite => false;
|
// public override bool CanWrite => false;
|
||||||
|
|
||||||
public override long Length => (long)CurrentBufferSize;
|
// public override long Length => (long)CurrentBufferSize;
|
||||||
|
|
||||||
public override long Position { get; set; } = 0;
|
// public override long Position { get; set; } = 0;
|
||||||
|
|
||||||
public override void Flush() { }
|
// public override void Flush() { }
|
||||||
|
|
||||||
public override int Read(byte[] buffer, int offset, int count)
|
// public override int Read(byte[] buffer, int offset, int count)
|
||||||
{
|
// {
|
||||||
int read = CurrentFileStream.Read(buffer, offset, count);
|
// int read = CurrentFileStream.Read(buffer, offset, count);
|
||||||
if (read < count)
|
// if (read < count)
|
||||||
{
|
// {
|
||||||
if (!BufferingCompleted || IsNextFileReady())
|
// if (!BufferingCompleted || IsNextFileReady())
|
||||||
{
|
// {
|
||||||
CurrentFileStream.Dispose();
|
// CurrentFileStream.Dispose();
|
||||||
CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
|
// CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
|
||||||
read += CurrentFileStream.Read(buffer, read + offset, count - read);
|
// read += CurrentFileStream.Read(buffer, read + offset, count - read);
|
||||||
}
|
// }
|
||||||
if (read < count)
|
// if (read < count)
|
||||||
Array.Clear(buffer, read, count - read);
|
// Array.Clear(buffer, read, count - read);
|
||||||
}
|
// }
|
||||||
return read;
|
// return read;
|
||||||
}
|
// }
|
||||||
|
|
||||||
public override long Seek(long offset, SeekOrigin origin)
|
// public override long Seek(long offset, SeekOrigin origin)
|
||||||
{
|
// {
|
||||||
throw new NotImplementedException();
|
// throw new NotImplementedException();
|
||||||
}
|
// }
|
||||||
|
|
||||||
public override void SetLength(long value)
|
// public override void SetLength(long value)
|
||||||
{
|
// {
|
||||||
throw new NotImplementedException();
|
// throw new NotImplementedException();
|
||||||
}
|
// }
|
||||||
|
|
||||||
public override void Write(byte[] buffer, int offset, int count)
|
// public override void Write(byte[] buffer, int offset, int count)
|
||||||
{
|
// {
|
||||||
throw new NotImplementedException();
|
// throw new NotImplementedException();
|
||||||
}
|
// }
|
||||||
|
|
||||||
public new void Dispose()
|
// public new void Dispose()
|
||||||
{
|
// {
|
||||||
CurrentFileStream.Dispose();
|
// CurrentFileStream.Dispose();
|
||||||
MusicPlayer.SongCancelSource.Cancel();
|
// MusicPlayer.SongCancelSource.Cancel();
|
||||||
CleanFiles();
|
// CleanFiles();
|
||||||
base.Dispose();
|
// base.Dispose();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
85
src/NadekoBot/Services/Music/SongInfo.cs
Normal file
85
src/NadekoBot/Services/Music/SongInfo.cs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
using Discord;
|
||||||
|
using NadekoBot.Extensions;
|
||||||
|
using NadekoBot.Services.Database.Models;
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
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 = 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.Normal:
|
||||||
|
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 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.Normal:
|
||||||
|
//todo have videoid in songinfo from the start
|
||||||
|
var videoId = videoIdRegex.Match(Query);
|
||||||
|
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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user