Removed module projects because it can't work like that atm. Commented out package commands.

This commit is contained in:
Master Kwoth
2017-10-15 09:39:46 +02:00
parent 90e71a3a30
commit 696a0eb2a7
180 changed files with 21625 additions and 1058 deletions

View File

@ -0,0 +1,9 @@
using System;
namespace NadekoBot.Modules.Music.Common.Exceptions
{
public class NotInVoiceChannelException : Exception
{
public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { }
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace NadekoBot.Modules.Music.Common.Exceptions
{
public class QueueFullException : Exception
{
public QueueFullException(string message) : base(message)
{
}
public QueueFullException() : base("Queue is full.") { }
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace NadekoBot.Modules.Music.Common.Exceptions
{
public class SongNotFoundException : Exception
{
public SongNotFoundException(string message) : base(message)
{
}
public SongNotFoundException() : base("Song is not found.") { }
}
}

View File

@ -0,0 +1,670 @@
using Discord;
using Discord.Audio;
using System;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using System.Linq;
using NadekoBot.Extensions;
using System.Diagnostics;
using NadekoBot.Common.Collections;
using NadekoBot.Modules.Music.Services;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
namespace NadekoBot.Modules.Music.Common
{
public enum StreamState
{
Resolving,
Queued,
Playing,
Completed
}
public class MusicPlayer
{
private readonly Thread _player;
public IVoiceChannel VoiceChannel { get; private set; }
private readonly Logger _log;
private MusicQueue Queue { get; } = new MusicQueue();
public bool Exited { get; set; } = false;
public bool Stopped { get; private set; } = false;
public float Volume { get; private set; } = 1.0f;
public bool Paused => pauseTaskSource != null;
private TaskCompletionSource<bool> pauseTaskSource { get; set; } = null;
public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%";
public string PrettyCurrentTime
{
get
{
var time = CurrentTime.ToString(@"mm\:ss");
var hrs = (int)CurrentTime.TotalHours;
if (hrs > 0)
return hrs + ":" + time;
else
return time;
}
}
public string PrettyFullTime => PrettyCurrentTime + " / " + (Queue.Current.Song?.PrettyTotalTime ?? "?");
private CancellationTokenSource SongCancelSource { get; set; }
public ITextChannel OutputTextChannel { get; set; }
public (int Index, SongInfo Current) Current
{
get
{
if (Stopped)
return (0, null);
return Queue.Current;
}
}
public bool RepeatCurrentSong { get; private set; }
public bool Shuffle { get; private set; }
public bool Autoplay { get; private set; }
public bool RepeatPlaylist { get; private set; } = false;
public uint MaxQueueSize
{
get => Queue.MaxQueueSize;
set { lock (locker) Queue.MaxQueueSize = value; }
}
private bool _fairPlay;
public bool FairPlay
{
get => _fairPlay;
set
{
if (value)
{
var cur = Queue.Current;
if (cur.Song != null)
RecentlyPlayedUsers.Add(cur.Song.QueuerName);
}
else
{
RecentlyPlayedUsers.Clear();
}
_fairPlay = value;
}
}
public bool AutoDelete { get; set; }
public uint MaxPlaytimeSeconds { get; set; }
const int _frameBytes = 3840;
const float _miliseconds = 20.0f;
public TimeSpan CurrentTime => TimeSpan.FromSeconds(_bytesSent / (float)_frameBytes / (1000 / _miliseconds));
private int _bytesSent = 0;
private IAudioClient _audioClient;
private readonly object locker = new object();
private MusicService _musicService;
#region events
public event Action<MusicPlayer, (int Index, SongInfo Song)> OnStarted;
public event Action<MusicPlayer, SongInfo> OnCompleted;
public event Action<MusicPlayer, bool> OnPauseChanged;
#endregion
private bool manualSkip = false;
private bool manualIndex = false;
private bool newVoiceChannel = false;
private readonly IGoogleApiService _google;
private bool cancel = false;
private ConcurrentHashSet<string> RecentlyPlayedUsers { get; } = new ConcurrentHashSet<string>();
public TimeSpan TotalPlaytime
{
get
{
var songs = Queue.ToArray().Songs;
return songs.Any(s => s.TotalTime == TimeSpan.MaxValue)
? TimeSpan.MaxValue
: new TimeSpan(songs.Sum(s => s.TotalTime.Ticks));
}
}
public MusicPlayer(MusicService musicService, IGoogleApiService google, IVoiceChannel vch, ITextChannel output, float volume)
{
_log = LogManager.GetCurrentClassLogger();
this.Volume = volume;
this.VoiceChannel = vch;
this.SongCancelSource = new CancellationTokenSource();
this.OutputTextChannel = output;
this._musicService = musicService;
this._google = google;
_log.Info("Initialized");
_player = new Thread(new ThreadStart(PlayerLoop));
_player.Start();
_log.Info("Loop started");
}
private async void PlayerLoop()
{
while (!Exited)
{
_bytesSent = 0;
cancel = false;
CancellationToken cancelToken;
(int Index, SongInfo Song) data;
lock (locker)
{
data = Queue.Current;
cancelToken = SongCancelSource.Token;
manualSkip = false;
manualIndex = false;
}
if (data.Song != null)
{
_log.Info("Starting");
AudioOutStream pcm = null;
SongBuffer b = null;
try
{
b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == MusicType.Local);
//_log.Info("Created buffer, buffering...");
//var bufferTask = b.StartBuffering(cancelToken);
//var timeout = Task.Delay(10000);
//if (Task.WhenAny(bufferTask, timeout) == timeout)
//{
// _log.Info("Buffering failed due to a timeout.");
// continue;
//}
//else if (!bufferTask.Result)
//{
// _log.Info("Buffering failed due to a cancel or error.");
// continue;
//}
//_log.Info("Buffered. Getting audio client...");
var ac = await GetAudioClient();
_log.Info("Got Audio client");
if (ac == null)
{
_log.Info("Can't join");
await Task.Delay(900, cancelToken);
// just wait some time, maybe bot doesn't even have perms to join that voice channel,
// i don't want to spam connection attempts
continue;
}
pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500);
_log.Info("Created pcm stream");
OnStarted?.Invoke(this, data);
byte[] buffer = new byte[3840];
int bytesRead = 0;
while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0
&& (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds))
{
AdjustVolume(buffer, Volume);
await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
unchecked { _bytesSent += bytesRead; }
await (pauseTaskSource?.Task ?? Task.CompletedTask);
}
}
catch (OperationCanceledException)
{
_log.Info("Song Canceled");
cancel = true;
}
catch (Exception ex)
{
_log.Warn(ex);
}
finally
{
if (pcm != null)
{
// flush is known to get stuck from time to time,
// just skip flushing if it takes more than 1 second
var flushCancel = new CancellationTokenSource();
var flushToken = flushCancel.Token;
var flushDelay = Task.Delay(1000, flushToken);
await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken));
flushCancel.Cancel();
pcm.Dispose();
}
if (b != null)
b.Dispose();
OnCompleted?.Invoke(this, data.Song);
if (_bytesSent == 0 && !cancel)
{
lock (locker)
Queue.RemoveSong(data.Song);
_log.Info("Song removed because it can't play");
}
}
try
{
//if repeating current song, just ignore other settings,
// and play this song again (don't change the index)
// ignore rcs if song is manually skipped
int queueCount;
bool stopped;
int currentIndex;
lock (locker)
{
queueCount = Queue.Count;
stopped = Stopped;
currentIndex = Queue.CurrentIndex;
}
if (AutoDelete && !RepeatCurrentSong && !RepeatPlaylist && data.Song != null)
{
Queue.RemoveSong(data.Song);
}
if (!manualIndex && (!RepeatCurrentSong || manualSkip))
{
if (Shuffle)
{
_log.Info("Random song");
Queue.Random(); //if shuffle is set, set current song index to a random number
}
else
{
//if last song, and autoplay is enabled, and if it's a youtube song
// do autplay magix
if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == MusicType.YouTube)
{
try
{
_log.Info("Loading related song");
await _musicService.TryQueueRelatedSongAsync(data.Song, OutputTextChannel, VoiceChannel);
if(!AutoDelete)
Queue.Next();
}
catch
{
_log.Info("Loading related song failed.");
}
}
else if (FairPlay)
{
lock (locker)
{
_log.Info("Next fair song");
var q = Queue.ToArray().Songs.Shuffle().ToArray();
bool found = false;
for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently
{
var item = q[i];
if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index
{
Queue.CurrentIndex = i;
found = true;
break;
}
}
if (!found) //if it's not
{
RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played)
Queue.Random(); //go to a random song (to prevent looping on the first few songs)
var cur = Current;
if (cur.Current != null) // add newely scheduled song's queuer to the recently played list
RecentlyPlayedUsers.Add(cur.Current.QueuerName);
}
}
}
else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip)
{
_log.Info("Stopping because repeatplaylist is disabled");
lock (locker)
{
Stop();
}
}
else
{
_log.Info("Next song");
lock (locker)
{
if (!Stopped)
if(!AutoDelete)
Queue.Next();
}
}
}
}
}
catch (Exception ex)
{
_log.Error(ex);
}
}
do
{
await Task.Delay(500);
}
while ((Queue.Count == 0 || Stopped) && !Exited);
}
}
private async Task<IAudioClient> GetAudioClient(bool reconnect = false)
{
if (_audioClient == null ||
_audioClient.ConnectionState != ConnectionState.Connected ||
reconnect ||
newVoiceChannel)
try
{
try
{
var t = _audioClient?.StopAsync();
if (t != null)
{
_log.Info("Stopping audio client");
await t;
_log.Info("Disposing audio client");
_audioClient.Dispose();
}
}
catch
{
}
newVoiceChannel = false;
_log.Info("Get current user");
var curUser = await VoiceChannel.Guild.GetCurrentUserAsync();
if (curUser.VoiceChannel != null)
{
_log.Info("Connecting");
var ac = await VoiceChannel.ConnectAsync();
_log.Info("Connected, stopping");
await ac.StopAsync();
_log.Info("Disconnected");
await Task.Delay(1000);
}
_log.Info("Connecting");
_audioClient = await VoiceChannel.ConnectAsync();
}
catch
{
return null;
}
return _audioClient;
}
public int Enqueue(SongInfo song)
{
lock (locker)
{
if (Exited)
return -1;
Queue.Add(song);
return Queue.Count - 1;
}
}
public int EnqueueNext(SongInfo song)
{
lock (locker)
{
if (Exited)
return -1;
return Queue.AddNext(song);
}
}
public void SetIndex(int index)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
lock (locker)
{
if (Exited)
return;
if (AutoDelete && index >= Queue.CurrentIndex && index > 0)
index--;
Queue.CurrentIndex = index;
manualIndex = true;
Stopped = false;
CancelCurrentSong();
}
}
public void Next(int skipCount = 1)
{
lock (locker)
{
if (Exited)
return;
manualSkip = true;
// if player is stopped, and user uses .n, it should play current song.
// It's a bit weird, but that's the least annoying solution
if (!Stopped)
if (!RepeatPlaylist && Queue.IsLast()) // if it's the last song in the queue, and repeat playlist is disabled
{ //stop the queue
Stop();
return;
}
else
Queue.Next(skipCount - 1);
else
Queue.CurrentIndex = 0;
Stopped = false;
CancelCurrentSong();
Unpause();
}
}
public void Stop(bool clearQueue = false)
{
lock (locker)
{
Stopped = true;
//Queue.ResetCurrent();
if (clearQueue)
Queue.Clear();
Unpause();
CancelCurrentSong();
}
}
private void Unpause()
{
lock (locker)
{
if (pauseTaskSource != null)
{
pauseTaskSource.TrySetResult(true);
pauseTaskSource = null;
}
}
}
public void TogglePause()
{
lock (locker)
{
if (pauseTaskSource == null)
pauseTaskSource = new TaskCompletionSource<bool>();
else
{
Unpause();
}
}
OnPauseChanged?.Invoke(this, pauseTaskSource != null);
}
public void SetVolume(int volume)
{
if (volume < 0 || volume > 100)
throw new ArgumentOutOfRangeException(nameof(volume));
lock (locker)
{
Volume = ((float)volume) / 100;
}
}
public SongInfo RemoveAt(int index)
{
lock (locker)
{
var cur = Queue.Current;
var toReturn = Queue.RemoveAt(index);
if (cur.Index == index)
Next();
return toReturn;
}
}
private void CancelCurrentSong()
{
lock (locker)
{
var cs = SongCancelSource;
SongCancelSource = new CancellationTokenSource();
cs.Cancel();
}
}
public void ClearQueue()
{
lock (locker)
{
Queue.Clear();
}
}
public (int CurrentIndex, SongInfo[] Songs) QueueArray()
{
lock (locker)
return Queue.ToArray();
}
//aidiakapi ftw
public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume)
{
if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples;
// 16-bit precision for the multiplication
var volumeFixed = (int)Math.Round(volume * 65536d);
var count = audioSamples.Length / 2;
fixed (byte* srcBytes = audioSamples)
{
var src = (short*)srcBytes;
for (var i = count; i != 0; i--, src++)
*src = (short)(((*src) * volumeFixed) >> 16);
}
return audioSamples;
}
public bool ToggleRepeatSong()
{
lock (locker)
{
return RepeatCurrentSong = !RepeatCurrentSong;
}
}
public async Task Destroy()
{
_log.Info("Destroying");
lock (locker)
{
Stop();
Exited = true;
Unpause();
OnCompleted = null;
OnPauseChanged = null;
OnStarted = null;
}
var ac = _audioClient;
if (ac != null)
await ac.StopAsync();
}
public bool ToggleShuffle()
{
lock (locker)
{
return Shuffle = !Shuffle;
}
}
public bool ToggleAutoplay()
{
lock (locker)
{
return Autoplay = !Autoplay;
}
}
public bool ToggleRepeatPlaylist()
{
lock (locker)
{
return RepeatPlaylist = !RepeatPlaylist;
}
}
public async Task SetVoiceChannel(IVoiceChannel vch)
{
lock (locker)
{
if (Exited)
return;
VoiceChannel = vch;
}
_audioClient = await vch.ConnectAsync();
}
public async Task UpdateSongDurationsAsync()
{
var sw = Stopwatch.StartNew();
var (_, songs) = Queue.ToArray();
var toUpdate = songs
.Where(x => x.ProviderType == MusicType.YouTube
&& x.TotalTime == TimeSpan.Zero);
var vIds = toUpdate.Select(x => x.VideoId);
sw.Stop();
_log.Info(sw.Elapsed.TotalSeconds);
if (!vIds.Any())
return;
var durations = await _google.GetVideoDurationsAsync(vIds);
foreach (var x in toUpdate)
{
if (durations.TryGetValue(x.VideoId, out var dur))
x.TotalTime = dur;
}
}
public SongInfo MoveSong(int n1, int n2)
=> Queue.MoveSong(n1, n2);
//// this should be written better
//public TimeSpan TotalPlaytime =>
// _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ?
// TimeSpan.MaxValue :
// new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks));
}
}

View File

@ -0,0 +1,215 @@
using NadekoBot.Extensions;
using NadekoBot.Modules.Music.Common.Exceptions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common;
namespace NadekoBot.Modules.Music.Common
{
public class MusicQueue : IDisposable
{
private LinkedList<SongInfo> Songs { get; set; } = new LinkedList<SongInfo>();
private int _currentIndex = 0;
public int CurrentIndex
{
get
{
return _currentIndex;
}
set
{
lock (locker)
{
if (Songs.Count == 0)
_currentIndex = 0;
else
_currentIndex = value %= Songs.Count;
}
}
}
public (int Index, SongInfo Song) Current
{
get
{
var cur = CurrentIndex;
return (cur, Songs.ElementAtOrDefault(cur));
}
}
private readonly object locker = new object();
private TaskCompletionSource<bool> nextSource { get; } = new TaskCompletionSource<bool>();
public int Count
{
get
{
lock (locker)
{
return Songs.Count;
}
}
}
private uint _maxQueueSize;
public uint MaxQueueSize
{
get => _maxQueueSize;
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value));
lock (locker)
{
_maxQueueSize = value;
}
}
}
public void Add(SongInfo song)
{
song.ThrowIfNull(nameof(song));
lock (locker)
{
if(MaxQueueSize != 0 && Songs.Count >= MaxQueueSize)
throw new QueueFullException();
Songs.AddLast(song);
}
}
public int AddNext(SongInfo song)
{
song.ThrowIfNull(nameof(song));
lock (locker)
{
if (MaxQueueSize != 0 && Songs.Count >= MaxQueueSize)
throw new QueueFullException();
var curSong = Current.Song;
if (curSong == null)
{
Songs.AddLast(song);
return Songs.Count;
}
var songlist = Songs.ToList();
songlist.Insert(CurrentIndex + 1, song);
Songs = new LinkedList<SongInfo>(songlist);
return CurrentIndex + 1;
}
}
public void Next(int skipCount = 1)
{
lock(locker)
CurrentIndex += skipCount;
}
public void Dispose()
{
Clear();
}
public SongInfo RemoveAt(int index)
{
lock (locker)
{
if (index < 0 || index >= Songs.Count)
throw new ArgumentOutOfRangeException(nameof(index));
var current = Songs.First.Value;
for (int i = 0; i < Songs.Count; i++)
{
if (i == index)
{
current = Songs.ElementAt(index);
Songs.Remove(current);
if (CurrentIndex != 0)
{
if (CurrentIndex >= index)
{
--CurrentIndex;
}
}
break;
}
}
return current;
}
}
public void Clear()
{
lock (locker)
{
Songs.Clear();
CurrentIndex = 0;
}
}
public (int CurrentIndex, SongInfo[] Songs) ToArray()
{
lock (locker)
{
return (CurrentIndex, Songs.ToArray());
}
}
public void ResetCurrent()
{
lock (locker)
{
CurrentIndex = 0;
}
}
public void Random()
{
lock (locker)
{
CurrentIndex = new NadekoRandom().Next(Songs.Count);
}
}
public SongInfo MoveSong(int n1, int n2)
{
lock (locker)
{
var currentSong = Current.Song;
var playlist = Songs.ToList();
if (n1 >= playlist.Count || n2 >= playlist.Count || n1 == n2)
return null;
var s = playlist[n1];
playlist.RemoveAt(n1);
playlist.Insert(n2, s);
Songs = new LinkedList<SongInfo>(playlist);
if (currentSong != null)
CurrentIndex = playlist.IndexOf(currentSong);
return s;
}
}
public void RemoveSong(SongInfo song)
{
lock (locker)
{
Songs.Remove(song);
}
}
public bool IsLast()
{
lock (locker)
return CurrentIndex == Songs.Count - 1;
}
}
}
//O O [O] O O O O
//
// 3

View File

@ -0,0 +1,95 @@
using NLog;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
namespace NadekoBot.Modules.Music.Common
{
public class SongBuffer : IDisposable
{
const int readSize = 81920;
private Process p;
private Stream _outStream;
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private readonly Logger _log;
public string SongUri { get; private set; }
public SongBuffer(string songUri, string skipTo, bool isLocal)
{
_log = LogManager.GetCurrentClassLogger();
this.SongUri = songUri;
this._isLocal = isLocal;
try
{
this.p = StartFFmpegProcess(SongUri, 0);
this._outStream = this.p.StandardOutput.BaseStream;
}
catch (System.ComponentModel.Win32Exception)
{
_log.Error(@"You have not properly installed or configured FFMPEG.
Please install and configure FFMPEG to play music.
Check the guides for your platform on how to setup ffmpeg correctly:
Windows Guide: https://goo.gl/OjKk8F
Linux Guide: https://goo.gl/ShjCUo");
}
catch (OperationCanceledException) { }
catch (InvalidOperationException) { } // when ffmpeg is disposed
catch (Exception ex)
{
_log.Info(ex);
}
}
private Process StartFFmpegProcess(string songUri, float skipTo = 0)
{
var args = $"-err_detect ignore_err -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error";
if (!_isLocal)
args = "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 " + args;
return Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = false,
CreateNoWindow = true,
});
}
private readonly object locker = new object();
private readonly bool _isLocal;
public int Read(byte[] b, int offset, int toRead)
{
lock (locker)
return _outStream.Read(b, offset, toRead);
}
public void Dispose()
{
try
{
this.p.StandardOutput.Dispose();
}
catch (Exception ex)
{
_log.Error(ex);
}
try
{
if(!this.p.HasExited)
this.p.Kill();
}
catch
{
}
_outStream.Dispose();
this.p.Dispose();
}
}
}

View File

@ -0,0 +1,9 @@
using NLog;
namespace NadekoBot.Modules.Music.Common
{
public static class SongHandler
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
}
}

View File

@ -0,0 +1,79 @@
using Discord;
using NadekoBot.Extensions;
using NadekoBot.Core.Services.Database.Models;
using System;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common
{
public class SongInfo
{
public string Provider { get; set; }
public MusicType ProviderType { get; set; }
public string Query { get; set; }
public string Title { get; set; }
public Func<Task<string>> Uri { get; set; }
public string Thumbnail { get; set; }
public string QueuerName { get; set; }
public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
public string PrettyProvider => (Provider ?? "???");
//public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime;
public string PrettyName => $"**[{Title.TrimTo(65)}]({SongUrl})**";
public string PrettyInfo => $"{PrettyTotalTime} | {PrettyProvider} | {QueuerName}";
public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {Format.Sanitize(QueuerName.TrimTo(15))}`";
public string PrettyTotalTime
{
get
{
if (TotalTime == TimeSpan.Zero)
return "(?)";
if (TotalTime == TimeSpan.MaxValue)
return "∞";
var time = TotalTime.ToString(@"mm\:ss");
var hrs = (int)TotalTime.TotalHours;
if (hrs > 0)
return hrs + ":" + time;
return time;
}
}
public string SongUrl
{
get
{
switch (ProviderType)
{
case MusicType.YouTube:
return Query;
case MusicType.Soundcloud:
return Query;
case MusicType.Local:
return $"https://google.com/search?q={ WebUtility.UrlEncode(Title).Replace(' ', '+') }";
case MusicType.Radio:
return $"https://google.com/search?q={Title}";
default:
return "";
}
}
}
private string _videoId = null;
public string VideoId
{
get
{
if (ProviderType == MusicType.YouTube)
return _videoId = _videoId ?? videoIdRegex.Match(Query)?.ToString();
return _videoId ?? "";
}
set => _videoId = value;
}
private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled);
}
}

View File

@ -0,0 +1,11 @@
using NadekoBot.Modules.Music.Common.SongResolver.Strategies;
using NadekoBot.Core.Services.Database.Models;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver
{
public interface ISongResolverFactory
{
Task<IResolveStrategy> GetResolveStrategy(string query, MusicType? musicType);
}
}

View File

@ -0,0 +1,41 @@
using System.Threading.Tasks;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Impl;
using NadekoBot.Modules.Music.Common.SongResolver.Strategies;
namespace NadekoBot.Modules.Music.Common.SongResolver
{
public class SongResolverFactory : ISongResolverFactory
{
private readonly SoundCloudApiService _sc;
public SongResolverFactory(SoundCloudApiService sc)
{
_sc = sc;
}
public async Task<IResolveStrategy> GetResolveStrategy(string query, MusicType? musicType)
{
await Task.Yield(); //for async warning
switch (musicType)
{
case MusicType.YouTube:
return new YoutubeResolveStrategy();
case MusicType.Radio:
return new RadioResolveStrategy();
case MusicType.Local:
return new LocalSongResolveStrategy();
case MusicType.Soundcloud:
return new SoundcloudResolveStrategy(_sc);
default:
if (_sc.IsSoundCloudLink(query))
return new SoundcloudResolveStrategy(_sc);
else if (RadioResolveStrategy.IsRadioLink(query))
return new RadioResolveStrategy();
// maybe add a check for local files in the future
else
return new YoutubeResolveStrategy();
}
}
}
}

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
{
public interface IResolveStrategy
{
Task<SongInfo> ResolveSong(string query);
}
}

View File

@ -0,0 +1,22 @@
using NadekoBot.Core.Services.Database.Models;
using System.IO;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
{
public class LocalSongResolveStrategy : IResolveStrategy
{
public Task<SongInfo> ResolveSong(string query)
{
return Task.FromResult(new SongInfo
{
Uri = () => Task.FromResult("\"" + Path.GetFullPath(query) + "\""),
Title = Path.GetFileNameWithoutExtension(query),
Provider = "Local File",
ProviderType = MusicType.Local,
Query = query,
Thumbnail = "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png",
});
}
}
}

View File

@ -0,0 +1,138 @@
using NadekoBot.Core.Services.Database.Models;
using NLog;
using System;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
{
public class RadioResolveStrategy : IResolveStrategy
{
private readonly Regex plsRegex = new Regex("File1=(?<url>.*?)\\n", RegexOptions.Compiled);
private readonly Regex m3uRegex = new Regex("(?<url>^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline);
private readonly Regex asxRegex = new Regex("<ref href=\"(?<url>.*?)\"", RegexOptions.Compiled);
private readonly Regex xspfRegex = new Regex("<location>(?<url>.*?)</location>", RegexOptions.Compiled);
private readonly Logger _log;
public RadioResolveStrategy()
{
_log = LogManager.GetCurrentClassLogger();
}
public async Task<SongInfo> ResolveSong(string query)
{
if (IsRadioLink(query))
query = await HandleStreamContainers(query);
return new SongInfo
{
Uri = () => Task.FromResult(query),
Title = query,
Provider = "Radio Stream",
ProviderType = MusicType.Radio,
Query = query,
TotalTime = TimeSpan.MaxValue,
Thumbnail = "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png",
};
}
public static bool IsRadioLink(string query) =>
(query.StartsWith("http") ||
query.StartsWith("ww"))
&&
(query.Contains(".pls") ||
query.Contains(".m3u") ||
query.Contains(".asx") ||
query.Contains(".xspf"));
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 = plsRegex.Match(file);
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 = m3uRegex.Match(file);
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 = asxRegex.Match(file);
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 = xspfRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
_log.Warn($"Failed reading .xspf:\n{file}");
return null;
}
}
return query;
}
}
}

View File

@ -0,0 +1,27 @@
using NadekoBot.Modules.Music.Extensions;
using NadekoBot.Core.Services.Impl;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
{
public class SoundcloudResolveStrategy : IResolveStrategy
{
private readonly SoundCloudApiService _sc;
public SoundcloudResolveStrategy(SoundCloudApiService sc)
{
_sc = sc;
}
public async Task<SongInfo> ResolveSong(string query)
{
var svideo = !_sc.IsSoundCloudLink(query) ?
await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false) :
await _sc.ResolveVideoAsync(query).ConfigureAwait(false);
if (svideo == null)
return null;
return await svideo.GetSongInfo();
}
}
}

View File

@ -0,0 +1,69 @@
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Impl;
using NLog;
using System;
using System.Globalization;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
{
public class YoutubeResolveStrategy : IResolveStrategy
{
private readonly Logger _log;
public YoutubeResolveStrategy()
{
_log = LogManager.GetCurrentClassLogger();
}
public async Task<SongInfo> ResolveSong(string query)
{
_log.Info("Getting link");
string[] data;
try
{
using (var ytdl = new YtdlOperation())
{
data = (await ytdl.GetDataAsync(query)).Split('\n');
}
if (data.Length < 6)
{
_log.Info("No song found. Data less than 6");
return null;
}
TimeSpan time;
if (!TimeSpan.TryParseExact(data[4], new[] { "ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss" }, CultureInfo.InvariantCulture, out time))
time = TimeSpan.FromHours(24);
return new SongInfo()
{
Title = data[0],
VideoId = data[1],
Uri = async () =>
{
using (var ytdl = new YtdlOperation())
{
data = (await ytdl.GetDataAsync(query)).Split('\n');
}
if (data.Length < 6)
{
_log.Info("No song found. Data less than 6");
return null;
}
return data[2];
},
Thumbnail = data[3],
TotalTime = time,
Provider = "YouTube",
ProviderType = MusicType.YouTube,
Query = "https://youtube.com/watch?v=" + data[1],
};
}
catch (Exception ex)
{
_log.Warn(ex);
return null;
}
}
}
}