add files?
This commit is contained in:
parent
fd5916a54d
commit
aa69703768
281
src/NadekoBot/Modules/Music/Classes/MusicControls.cs
Normal file
281
src/NadekoBot/Modules/Music/Classes/MusicControls.cs
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
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;
|
||||||
|
namespace NadekoBot.Modules.Music.Classes
|
||||||
|
{
|
||||||
|
|
||||||
|
public enum MusicType
|
||||||
|
{
|
||||||
|
Radio,
|
||||||
|
Normal,
|
||||||
|
Local,
|
||||||
|
Soundcloud
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StreamState
|
||||||
|
{
|
||||||
|
Resolving,
|
||||||
|
Queued,
|
||||||
|
Playing,
|
||||||
|
Completed
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MusicPlayer
|
||||||
|
{
|
||||||
|
private IAudioClient audioClient { get; set; }
|
||||||
|
|
||||||
|
private readonly List<Song> playlist = new List<Song>();
|
||||||
|
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 EventHandler<Song> OnCompleted = delegate { };
|
||||||
|
public event EventHandler<Song> OnStarted = delegate { };
|
||||||
|
|
||||||
|
public IVoiceChannel PlaybackVoiceChannel { get; private set; }
|
||||||
|
|
||||||
|
private bool Destroyed { get; set; } = false;
|
||||||
|
public bool RepeatSong { get; private set; } = false;
|
||||||
|
public bool RepeatPlaylist { get; private set; } = false;
|
||||||
|
public bool Autoplay { get; set; } = false;
|
||||||
|
public uint MaxQueueSize { get; set; } = 0;
|
||||||
|
|
||||||
|
private ConcurrentQueue<Action> actionQueue { get; set; } = new ConcurrentQueue<Action>();
|
||||||
|
|
||||||
|
public MusicPlayer(IVoiceChannel startingVoiceChannel, float? defaultVolume)
|
||||||
|
{
|
||||||
|
if (startingVoiceChannel == null)
|
||||||
|
throw new ArgumentNullException(nameof(startingVoiceChannel));
|
||||||
|
Volume = defaultVolume ?? 1.0f;
|
||||||
|
|
||||||
|
PlaybackVoiceChannel = startingVoiceChannel;
|
||||||
|
SongCancelSource = new CancellationTokenSource();
|
||||||
|
cancelToken = SongCancelSource.Token;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!Destroyed)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Action action;
|
||||||
|
if (actionQueue.TryDequeue(out action))
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await Task.Delay(100).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Action queue crashed");
|
||||||
|
Console.WriteLine(ex);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var t = new Thread(new ThreadStart(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!Destroyed)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (audioClient?.ConnectionState != ConnectionState.Connected)
|
||||||
|
{
|
||||||
|
audioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentSong = GetNextSong();
|
||||||
|
RemoveSongAt(0);
|
||||||
|
|
||||||
|
if (CurrentSong == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
|
||||||
|
OnStarted(this, CurrentSong);
|
||||||
|
await CurrentSong.Play(audioClient, cancelToken);
|
||||||
|
|
||||||
|
OnCompleted(this, CurrentSong);
|
||||||
|
|
||||||
|
if (RepeatPlaylist)
|
||||||
|
AddSong(CurrentSong, CurrentSong.QueuerName);
|
||||||
|
|
||||||
|
if (RepeatSong)
|
||||||
|
AddSong(CurrentSong, 0);
|
||||||
|
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!cancelToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
SongCancelSource.Cancel();
|
||||||
|
}
|
||||||
|
SongCancelSource = new CancellationTokenSource();
|
||||||
|
cancelToken = SongCancelSource.Token;
|
||||||
|
CurrentSong = null;
|
||||||
|
await Task.Delay(300).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Console.WriteLine("Music thread crashed.");
|
||||||
|
Console.WriteLine(ex);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
t.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Next()
|
||||||
|
{
|
||||||
|
actionQueue.Enqueue(() =>
|
||||||
|
{
|
||||||
|
Paused = false;
|
||||||
|
SongCancelSource.Cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
actionQueue.Enqueue(() =>
|
||||||
|
{
|
||||||
|
RepeatPlaylist = false;
|
||||||
|
RepeatSong = false;
|
||||||
|
playlist.Clear();
|
||||||
|
if (!SongCancelSource.IsCancellationRequested)
|
||||||
|
SongCancelSource.Cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TogglePause() => 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() =>
|
||||||
|
playlist.FirstOrDefault();
|
||||||
|
|
||||||
|
public void Shuffle()
|
||||||
|
{
|
||||||
|
actionQueue.Enqueue(() =>
|
||||||
|
{
|
||||||
|
playlist.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)
|
||||||
|
{
|
||||||
|
actionQueue.Enqueue(() =>
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= playlist.Count)
|
||||||
|
return;
|
||||||
|
playlist.RemoveAt(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ClearQueue()
|
||||||
|
{
|
||||||
|
actionQueue.Enqueue(() =>
|
||||||
|
{
|
||||||
|
playlist.Clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Destroy()
|
||||||
|
{
|
||||||
|
actionQueue.Enqueue(async () =>
|
||||||
|
{
|
||||||
|
RepeatPlaylist = false;
|
||||||
|
RepeatSong = false;
|
||||||
|
Destroyed = true;
|
||||||
|
playlist.Clear();
|
||||||
|
if (!SongCancelSource.IsCancellationRequested)
|
||||||
|
SongCancelSource.Cancel();
|
||||||
|
await audioClient.DisconnectAsync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal 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;
|
||||||
|
return PlaybackVoiceChannel.ConnectAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool ToggleRepeatSong() => this.RepeatSong = !this.RepeatSong;
|
||||||
|
|
||||||
|
internal bool ToggleRepeatPlaylist() => this.RepeatPlaylist = !this.RepeatPlaylist;
|
||||||
|
|
||||||
|
internal bool ToggleAutoplay() => this.Autoplay = !this.Autoplay;
|
||||||
|
|
||||||
|
internal void ThrowIfQueueFull()
|
||||||
|
{
|
||||||
|
if (MaxQueueSize == 0)
|
||||||
|
return;
|
||||||
|
if (playlist.Count >= MaxQueueSize)
|
||||||
|
throw new PlaylistFullException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/NadekoBot/Modules/Music/Classes/PlaylistFullException.cs
Normal file
12
src/NadekoBot/Modules/Music/Classes/PlaylistFullException.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Music.Classes
|
||||||
|
{
|
||||||
|
class PlaylistFullException : Exception
|
||||||
|
{
|
||||||
|
public PlaylistFullException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public PlaylistFullException() : base("Queue is full.") { }
|
||||||
|
}
|
||||||
|
}
|
422
src/NadekoBot/Modules/Music/Classes/Song.cs
Normal file
422
src/NadekoBot/Modules/Music/Classes/Song.cs
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
using Discord.Audio;
|
||||||
|
using NadekoBot.Classes;
|
||||||
|
using NadekoBot.Extensions;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using VideoLibrary;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Music.Classes
|
||||||
|
{
|
||||||
|
public class SongInfo
|
||||||
|
{
|
||||||
|
public string Provider { get; internal set; }
|
||||||
|
public MusicType ProviderType { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Will be set only if the providertype is normal
|
||||||
|
/// </summary>
|
||||||
|
public string Query { get; internal set; }
|
||||||
|
public string Title { get; internal set; }
|
||||||
|
public string Uri { get; internal set; }
|
||||||
|
}
|
||||||
|
public class Song
|
||||||
|
{
|
||||||
|
public StreamState State { get; internal set; }
|
||||||
|
public string PrettyName =>
|
||||||
|
$"**【 {SongInfo.Title.TrimTo(55)} 】**`{(SongInfo.Provider ?? "-")}` `by {QueuerName}`";
|
||||||
|
public SongInfo SongInfo { get; }
|
||||||
|
public string QueuerName { get; set; }
|
||||||
|
|
||||||
|
public MusicPlayer MusicPlayer { get; set; }
|
||||||
|
|
||||||
|
public string PrettyCurrentTime()
|
||||||
|
{
|
||||||
|
var time = TimeSpan.FromSeconds(bytesSent / 3840 / 50);
|
||||||
|
return $"【{(int)time.TotalMinutes}m {time.Seconds}s】";
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong bytesSent { get; set; } = 0;
|
||||||
|
|
||||||
|
public bool PrintStatusMessage { get; set; } = true;
|
||||||
|
|
||||||
|
private int skipTo = 0;
|
||||||
|
public int SkipTo {
|
||||||
|
get { return SkipTo; }
|
||||||
|
set {
|
||||||
|
skipTo = value;
|
||||||
|
bytesSent = (ulong)skipTo * 3840 * 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Song(SongInfo songInfo)
|
||||||
|
{
|
||||||
|
this.SongInfo = songInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Song Clone()
|
||||||
|
{
|
||||||
|
var s = new Song(SongInfo);
|
||||||
|
s.MusicPlayer = MusicPlayer;
|
||||||
|
s.State = StreamState.Queued;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Song SetMusicPlayer(MusicPlayer mp)
|
||||||
|
{
|
||||||
|
this.MusicPlayer = mp;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
var filename = Path.Combine(Music.MusicDataPath, DateTime.Now.UnixTimestamp().ToString());
|
||||||
|
|
||||||
|
SongBuffer sb = new SongBuffer(filename, SongInfo, skipTo);
|
||||||
|
var bufferTask = sb.BufferSong(cancelToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var inStream = new FileStream(sb.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); ;
|
||||||
|
|
||||||
|
bytesSent = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var attempt = 0;
|
||||||
|
|
||||||
|
var prebufferingTask = CheckPrebufferingAsync(inStream, sb, cancelToken);
|
||||||
|
var sw = new Stopwatch();
|
||||||
|
sw.Start();
|
||||||
|
var t = await Task.WhenAny(prebufferingTask, Task.Delay(5000, cancelToken));
|
||||||
|
if (t != prebufferingTask)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Prebuffering timed out or canceled. Cannot get any data from the stream.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if(prebufferingTask.IsCanceled)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Prebuffering timed out. Cannot get any data from the stream.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sw.Stop();
|
||||||
|
Console.WriteLine("Prebuffering successfully completed in "+ sw.Elapsed);
|
||||||
|
|
||||||
|
|
||||||
|
var outStream = voiceClient.CreatePCMStream(3840);
|
||||||
|
|
||||||
|
const int blockSize = 3840;
|
||||||
|
byte[] buffer = new byte[blockSize];
|
||||||
|
while (!cancelToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
//Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------");
|
||||||
|
var read = inStream.Read(buffer, 0, buffer.Length);
|
||||||
|
//await inStream.CopyToAsync(voiceClient.OutputStream);
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
bytesSent += (ulong)read;
|
||||||
|
}
|
||||||
|
if (read < blockSize)
|
||||||
|
{
|
||||||
|
if (sb.IsNextFileReady())
|
||||||
|
{
|
||||||
|
inStream.Dispose();
|
||||||
|
inStream = new FileStream(sb.GetNextFile(), FileMode.Open, FileAccess.Read, FileShare.Write);
|
||||||
|
read += inStream.Read(buffer, read, buffer.Length - read);
|
||||||
|
attempt = 0;
|
||||||
|
}
|
||||||
|
if (read == 0)
|
||||||
|
{
|
||||||
|
if (sb.BufferingCompleted)
|
||||||
|
break;
|
||||||
|
if (attempt++ == 20)
|
||||||
|
{
|
||||||
|
MusicPlayer.SongCancelSource.Cancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await Task.Delay(100, cancelToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
attempt = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
attempt = 0;
|
||||||
|
|
||||||
|
while (this.MusicPlayer.Paused)
|
||||||
|
await Task.Delay(200, cancelToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
buffer = AdjustVolume(buffer, MusicPlayer.Volume);
|
||||||
|
await outStream.WriteAsync(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await bufferTask;
|
||||||
|
if(inStream != null)
|
||||||
|
inStream.Dispose();
|
||||||
|
Console.WriteLine("l");
|
||||||
|
sb.CleanFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckPrebufferingAsync(Stream inStream, SongBuffer sb, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
while (!sb.BufferingCompleted && inStream.Length < 2.MiB())
|
||||||
|
{
|
||||||
|
await Task.Delay(100, cancelToken);
|
||||||
|
}
|
||||||
|
Console.WriteLine("Buffering successfull");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
//stackoverflow ftw
|
||||||
|
private static byte[] AdjustVolume(byte[] audioSamples, float volume)
|
||||||
|
{
|
||||||
|
if (Math.Abs(volume - 1.0f) < 0.01f)
|
||||||
|
return audioSamples;
|
||||||
|
var array = new byte[audioSamples.Length];
|
||||||
|
for (var i = 0; i < array.Length; i += 2)
|
||||||
|
{
|
||||||
|
|
||||||
|
// convert byte pair to int
|
||||||
|
short buf1 = audioSamples[i + 1];
|
||||||
|
short buf2 = audioSamples[i];
|
||||||
|
|
||||||
|
buf1 = (short)((buf1 & 0xff) << 8);
|
||||||
|
buf2 = (short)(buf2 & 0xff);
|
||||||
|
|
||||||
|
var res = (short)(buf1 | buf2);
|
||||||
|
res = (short)(res * volume);
|
||||||
|
|
||||||
|
// convert back
|
||||||
|
array[i] = (byte)res;
|
||||||
|
array[i + 1] = (byte)(res >> 8);
|
||||||
|
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
//aidiakapi ftw
|
||||||
|
public unsafe static byte[] AdjustVolume(byte[] audioSamples, float volume)
|
||||||
|
{
|
||||||
|
Contract.Requires(audioSamples != null);
|
||||||
|
Contract.Requires(audioSamples.Length % 2 == 0);
|
||||||
|
Contract.Requires(volume >= 0f && volume <= 1f);
|
||||||
|
Contract.Assert(BitConverter.IsLittleEndian);
|
||||||
|
|
||||||
|
if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples;
|
||||||
|
|
||||||
|
// 16-bit precision for the multiplication
|
||||||
|
int volumeFixed = (int)Math.Round(volume * 65536d);
|
||||||
|
|
||||||
|
int count = audioSamples.Length / 2;
|
||||||
|
|
||||||
|
fixed (byte* srcBytes = audioSamples)
|
||||||
|
{
|
||||||
|
short* src = (short*)srcBytes;
|
||||||
|
|
||||||
|
for (int i = count; i != 0; i--, src++)
|
||||||
|
*src = (short)(((*src) * volumeFixed) >> 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
return audioSamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (SoundCloud.Default.IsSoundCloudLink(query))
|
||||||
|
{
|
||||||
|
var svideo = await SoundCloud.Default.ResolveVideoAsync(query).ConfigureAwait(false);
|
||||||
|
return new Song(new SongInfo
|
||||||
|
{
|
||||||
|
Title = svideo.FullName,
|
||||||
|
Provider = "SoundCloud",
|
||||||
|
Uri = svideo.StreamLink,
|
||||||
|
ProviderType = musicType,
|
||||||
|
Query = svideo.TrackLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (musicType == MusicType.Soundcloud)
|
||||||
|
{
|
||||||
|
var svideo = await SoundCloud.Default.GetVideoByQueryAsync(query).ConfigureAwait(false);
|
||||||
|
return new Song(new SongInfo
|
||||||
|
{
|
||||||
|
Title = svideo.FullName,
|
||||||
|
Provider = "SoundCloud",
|
||||||
|
Uri = svideo.StreamLink,
|
||||||
|
ProviderType = MusicType.Normal,
|
||||||
|
Query = svideo.TrackLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var link = (await NadekoBot.Google.GetVideosByKeywordsAsync(query).ConfigureAwait(false)).FirstOrDefault();
|
||||||
|
if (string.IsNullOrWhiteSpace(link))
|
||||||
|
throw new OperationCanceledException("Not a valid youtube query.");
|
||||||
|
var allVideos = await Task.Run(async () => await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false)).ConfigureAwait(false);
|
||||||
|
var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio);
|
||||||
|
var video = videos
|
||||||
|
.Where(v => v.AudioBitrate < 192)
|
||||||
|
.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 = video.Uri,
|
||||||
|
Query = link,
|
||||||
|
ProviderType = musicType,
|
||||||
|
});
|
||||||
|
song.SkipTo = gotoTime;
|
||||||
|
return song;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed resolving the link.{ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static 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
|
||||||
|
{
|
||||||
|
Console.WriteLine($"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
|
||||||
|
{
|
||||||
|
Console.WriteLine($"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
|
||||||
|
{
|
||||||
|
Console.WriteLine($"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
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed reading .xspf:\n{file}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRadioLink(string query) =>
|
||||||
|
(query.StartsWith("http") ||
|
||||||
|
query.StartsWith("ww"))
|
||||||
|
&&
|
||||||
|
(query.Contains(".pls") ||
|
||||||
|
query.Contains(".m3u") ||
|
||||||
|
query.Contains(".asx") ||
|
||||||
|
query.Contains(".xspf"));
|
||||||
|
}
|
||||||
|
}
|
159
src/NadekoBot/Modules/Music/Classes/SongBuffer.cs
Normal file
159
src/NadekoBot/Modules/Music/Classes/SongBuffer.cs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
using NadekoBot.Extensions;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Music.Classes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space.
|
||||||
|
/// It also help for large music by deleting files that are already seen.
|
||||||
|
/// </summary>
|
||||||
|
class SongBuffer
|
||||||
|
{
|
||||||
|
|
||||||
|
public SongBuffer(string basename, SongInfo songInfo, int skipTo)
|
||||||
|
{
|
||||||
|
Basename = basename;
|
||||||
|
SongInfo = songInfo;
|
||||||
|
SkipTo = skipTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Basename;
|
||||||
|
|
||||||
|
private SongInfo SongInfo;
|
||||||
|
|
||||||
|
private int SkipTo;
|
||||||
|
|
||||||
|
private static int MAX_FILE_SIZE = 20.MiB();
|
||||||
|
|
||||||
|
private long FileNumber = -1;
|
||||||
|
|
||||||
|
private long NextFileToRead = 0;
|
||||||
|
|
||||||
|
public bool BufferingCompleted { get; private set;} = false;
|
||||||
|
|
||||||
|
private ulong CurrentBufferSize = 0;
|
||||||
|
|
||||||
|
public Task BufferSong(CancellationToken cancelToken) =>
|
||||||
|
Task.Factory.StartNew(async () =>
|
||||||
|
{
|
||||||
|
Process p = null;
|
||||||
|
FileStream outStream = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
p = Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "ffmpeg",
|
||||||
|
Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -ac 2 pipe:1 -loglevel quiet",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
byte[] buffer = new byte[81920];
|
||||||
|
int currentFileSize = 0;
|
||||||
|
ulong prebufferSize = 100ul.MiB();
|
||||||
|
|
||||||
|
outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
|
||||||
|
while (!p.HasExited) //Also fix low bandwidth
|
||||||
|
{
|
||||||
|
int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false);
|
||||||
|
if (currentFileSize >= MAX_FILE_SIZE)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
outStream.Dispose();
|
||||||
|
}catch { }
|
||||||
|
outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
|
||||||
|
currentFileSize = bytesRead;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentFileSize += bytesRead;
|
||||||
|
}
|
||||||
|
CurrentBufferSize += Convert.ToUInt64(bytesRead);
|
||||||
|
await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
|
||||||
|
while (CurrentBufferSize > prebufferSize)
|
||||||
|
await Task.Delay(100, cancelToken);
|
||||||
|
}
|
||||||
|
BufferingCompleted = true;
|
||||||
|
}
|
||||||
|
catch (System.ComponentModel.Win32Exception)
|
||||||
|
{
|
||||||
|
var oldclr = Console.ForegroundColor;
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
Console.WriteLine(@"You have not properly installed or configured FFMPEG.
|
||||||
|
Please install and configure FFMPEG to play music.
|
||||||
|
Check the guides for your platform on how to setup ffmpeg correctly:
|
||||||
|
Windows Guide: https://goo.gl/SCv72y
|
||||||
|
Linux Guide: https://goo.gl/rRhjCp");
|
||||||
|
Console.ForegroundColor = oldclr;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Buffering stopped: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if(outStream != null)
|
||||||
|
outStream.Dispose();
|
||||||
|
Console.WriteLine($"Buffering done.");
|
||||||
|
if (p != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
p.Kill();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
p.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, TaskCreationOptions.LongRunning);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return the next file to read, and delete the old one
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Name of the file to read</returns>
|
||||||
|
public string GetNextFile()
|
||||||
|
{
|
||||||
|
string filename = Basename + "-" + NextFileToRead;
|
||||||
|
|
||||||
|
if (NextFileToRead != 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length);
|
||||||
|
File.Delete(Basename + "-" + (NextFileToRead - 1));
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
NextFileToRead++;
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsNextFileReady()
|
||||||
|
{
|
||||||
|
return NextFileToRead <= FileNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanFiles()
|
||||||
|
{
|
||||||
|
for (long i = NextFileToRead - 1 ; i <= FileNumber; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(Basename + "-" + i);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
src/NadekoBot/Modules/Music/Classes/SoundCloud.cs
Normal file
141
src/NadekoBot/Modules/Music/Classes/SoundCloud.cs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
using NadekoBot.Classes;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Music.Classes
|
||||||
|
{
|
||||||
|
public class SoundCloud
|
||||||
|
{
|
||||||
|
private static readonly SoundCloud _instance = new SoundCloud();
|
||||||
|
public static SoundCloud Default => _instance;
|
||||||
|
|
||||||
|
static SoundCloud() { }
|
||||||
|
public SoundCloud() { }
|
||||||
|
|
||||||
|
public async Task<SoundCloudVideo> ResolveVideoAsync(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
throw new ArgumentNullException(nameof(url));
|
||||||
|
if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.SoundCloudClientId))
|
||||||
|
throw new ArgumentNullException(nameof(NadekoBot.Credentials.SoundCloudClientId));
|
||||||
|
|
||||||
|
string response = "";
|
||||||
|
|
||||||
|
using (var http = new HttpClient())
|
||||||
|
{
|
||||||
|
response = await http.GetStringAsync($"http://api.soundcloud.com/resolve?url={url}&client_id={NadekoBot.Credentials.SoundCloudClientId}").ConfigureAwait(false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var responseObj = Newtonsoft.Json.JsonConvert.DeserializeObject<SoundCloudVideo>(response);
|
||||||
|
if (responseObj?.Kind != "track")
|
||||||
|
throw new InvalidOperationException("Url is either not a track, or it doesn't exist.");
|
||||||
|
|
||||||
|
return responseObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSoundCloudLink(string url) =>
|
||||||
|
System.Text.RegularExpressions.Regex.IsMatch(url, "(.*)(soundcloud.com|snd.sc)(.*)");
|
||||||
|
|
||||||
|
internal async Task<SoundCloudVideo> GetVideoByQueryAsync(string query)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
|
throw new ArgumentNullException(nameof(query));
|
||||||
|
if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.SoundCloudClientId))
|
||||||
|
throw new ArgumentNullException(nameof(NadekoBot.Credentials.SoundCloudClientId));
|
||||||
|
|
||||||
|
var response = "";
|
||||||
|
using (var http = new HttpClient())
|
||||||
|
{
|
||||||
|
await http.GetStringAsync($"http://api.soundcloud.com/tracks?q={Uri.EscapeDataString(query)}&client_id={NadekoBot.Credentials.SoundCloudClientId}").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseObj = JsonConvert.DeserializeObject<SoundCloudVideo[]>(response).Where(s => s.Streamable).FirstOrDefault();
|
||||||
|
if (responseObj?.Kind != "track")
|
||||||
|
throw new InvalidOperationException("Query yielded no results.");
|
||||||
|
|
||||||
|
return responseObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SoundCloudVideo
|
||||||
|
{
|
||||||
|
public string Kind { get; set; } = "";
|
||||||
|
public long Id { get; set; } = 0;
|
||||||
|
public SoundCloudUser User { get; set; } = new SoundCloudUser();
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
[JsonIgnore]
|
||||||
|
public string FullName => User.Name + " - " + Title;
|
||||||
|
public bool Streamable { get; set; } = false;
|
||||||
|
[JsonProperty("permalink_url")]
|
||||||
|
public string TrackLink { get; set; } = "";
|
||||||
|
[JsonIgnore]
|
||||||
|
public string StreamLink => $"https://api.soundcloud.com/tracks/{Id}/stream?client_id={NadekoBot.Credentials.SoundCloudClientId}";
|
||||||
|
}
|
||||||
|
public class SoundCloudUser
|
||||||
|
{
|
||||||
|
[Newtonsoft.Json.JsonProperty("username")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
{"kind":"track",
|
||||||
|
"id":238888167,
|
||||||
|
"created_at":"2015/12/24 01:04:52 +0000",
|
||||||
|
"user_id":43141975,
|
||||||
|
"duration":120852,
|
||||||
|
"commentable":true,
|
||||||
|
"state":"finished",
|
||||||
|
"original_content_size":4834829,
|
||||||
|
"last_modified":"2015/12/24 01:17:59 +0000",
|
||||||
|
"sharing":"public",
|
||||||
|
"tag_list":"Funky",
|
||||||
|
"permalink":"18-fd",
|
||||||
|
"streamable":true,
|
||||||
|
"embeddable_by":"all",
|
||||||
|
"downloadable":false,
|
||||||
|
"purchase_url":null,
|
||||||
|
"label_id":null,
|
||||||
|
"purchase_title":null,
|
||||||
|
"genre":"Disco",
|
||||||
|
"title":"18 Ж",
|
||||||
|
"description":"",
|
||||||
|
"label_name":null,
|
||||||
|
"release":null,
|
||||||
|
"track_type":null,
|
||||||
|
"key_signature":null,
|
||||||
|
"isrc":null,
|
||||||
|
"video_url":null,
|
||||||
|
"bpm":null,
|
||||||
|
"release_year":null,
|
||||||
|
"release_month":null,
|
||||||
|
"release_day":null,
|
||||||
|
"original_format":"mp3",
|
||||||
|
"license":"all-rights-reserved",
|
||||||
|
"uri":"https://api.soundcloud.com/tracks/238888167",
|
||||||
|
"user":{
|
||||||
|
"id":43141975,
|
||||||
|
"kind":"user",
|
||||||
|
"permalink":"mrb00gi",
|
||||||
|
"username":"Mrb00gi",
|
||||||
|
"last_modified":"2015/12/01 16:06:57 +0000",
|
||||||
|
"uri":"https://api.soundcloud.com/users/43141975",
|
||||||
|
"permalink_url":"http://soundcloud.com/mrb00gi",
|
||||||
|
"avatar_url":"https://a1.sndcdn.com/images/default_avatar_large.png"
|
||||||
|
},
|
||||||
|
"permalink_url":"http://soundcloud.com/mrb00gi/18-fd",
|
||||||
|
"artwork_url":null,
|
||||||
|
"waveform_url":"https://w1.sndcdn.com/gsdLfvEW1cUK_m.png",
|
||||||
|
"stream_url":"https://api.soundcloud.com/tracks/238888167/stream",
|
||||||
|
"playback_count":7,
|
||||||
|
"download_count":0,
|
||||||
|
"favoritings_count":1,
|
||||||
|
"comment_count":0,
|
||||||
|
"attachments_uri":"https://api.soundcloud.com/tracks/238888167/attachments"}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user