mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 08:34:27 -05:00 
			
		
		
		
	Restructured folders and project names, ci should be fixed
This commit is contained in:
		
							
								
								
									
										14
									
								
								src/NadekoBot/Modules/Music/Common/ICachableTrackData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/NadekoBot/Modules/Music/Common/ICachableTrackData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface ICachableTrackData
 | 
			
		||||
    {
 | 
			
		||||
        string Id { get; set; }
 | 
			
		||||
        string Url { get; set; }
 | 
			
		||||
        string Thumbnail { get; set; }
 | 
			
		||||
        public TimeSpan Duration { get; }
 | 
			
		||||
        MusicPlatform Platform { get; set; }
 | 
			
		||||
        string Title { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface ILocalTrackResolver : IPlatformQueryResolver
 | 
			
		||||
    {
 | 
			
		||||
        IAsyncEnumerable<ITrackInfo> ResolveDirectoryAsync(string dirPath);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								src/NadekoBot/Modules/Music/Common/IMusicPlayer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/NadekoBot/Modules/Music/Common/IMusicPlayer.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
#nullable enable
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface IMusicPlayer : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        float Volume { get; }
 | 
			
		||||
        bool IsPaused { get; }
 | 
			
		||||
        bool IsStopped { get; }
 | 
			
		||||
        bool IsKilled { get; }
 | 
			
		||||
        int CurrentIndex { get; }
 | 
			
		||||
        public PlayerRepeatType Repeat { get; }
 | 
			
		||||
 | 
			
		||||
        void Stop();
 | 
			
		||||
        void Clear();
 | 
			
		||||
        IReadOnlyCollection<IQueuedTrackInfo> GetQueuedTracks();
 | 
			
		||||
        IQueuedTrackInfo? GetCurrentTrack(out int index);
 | 
			
		||||
        void Next();
 | 
			
		||||
        bool MoveTo(int index);
 | 
			
		||||
        void SetVolume(int newVolume);
 | 
			
		||||
 | 
			
		||||
        void Kill();
 | 
			
		||||
        bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo);
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
        Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync(string query, string queuer, bool asNext, MusicPlatform? forcePlatform = null);
 | 
			
		||||
        Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer);
 | 
			
		||||
        bool TogglePause();
 | 
			
		||||
        IQueuedTrackInfo? MoveTrack(int from, int to);
 | 
			
		||||
        void EnqueueTrack(ITrackInfo track, string queuer);
 | 
			
		||||
        void EnqueueTracks(IEnumerable<ITrackInfo> tracks, string queuer);
 | 
			
		||||
        void SetRepeat(PlayerRepeatType type);
 | 
			
		||||
        void ShuffleQueue();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/NadekoBot/Modules/Music/Common/IMusicQueue.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/NadekoBot/Modules/Music/Common/IMusicQueue.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface IMusicQueue
 | 
			
		||||
    {
 | 
			
		||||
        IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int index);
 | 
			
		||||
        IQueuedTrackInfo EnqueueNext(ITrackInfo song, string queuer, out int index);
 | 
			
		||||
        
 | 
			
		||||
        void EnqueueMany(IEnumerable<ITrackInfo> tracks, string queuer);
 | 
			
		||||
        
 | 
			
		||||
        public IReadOnlyCollection<IQueuedTrackInfo> List();
 | 
			
		||||
        IQueuedTrackInfo? GetCurrent(out int index);
 | 
			
		||||
        void Advance();
 | 
			
		||||
        void Clear();
 | 
			
		||||
        bool SetIndex(int index);
 | 
			
		||||
        bool TryRemoveAt(int index, out IQueuedTrackInfo? trackInfo, out bool isCurrent);
 | 
			
		||||
        int Index { get; }
 | 
			
		||||
        int Count { get; }
 | 
			
		||||
        void RemoveCurrent();
 | 
			
		||||
        IQueuedTrackInfo? MoveTrack(int from, int to);
 | 
			
		||||
        void Shuffle(Random rng);
 | 
			
		||||
        bool IsLast();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/NadekoBot/Modules/Music/Common/IPlatformQueryResolver.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/NadekoBot/Modules/Music/Common/IPlatformQueryResolver.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface IPlatformQueryResolver
 | 
			
		||||
    {
 | 
			
		||||
        Task<ITrackInfo?> ResolveByQueryAsync(string query);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/NadekoBot/Modules/Music/Common/IQueuedTrackInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/NadekoBot/Modules/Music/Common/IQueuedTrackInfo.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface IQueuedTrackInfo : ITrackInfo
 | 
			
		||||
    {
 | 
			
		||||
        public ITrackInfo TrackInfo { get; }
 | 
			
		||||
 | 
			
		||||
        public string Queuer { get; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/NadekoBot/Modules/Music/Common/IRadioResolver.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/NadekoBot/Modules/Music/Common/IRadioResolver.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface IRadioResolver : IPlatformQueryResolver
 | 
			
		||||
    {
 | 
			
		||||
        
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/NadekoBot/Modules/Music/Common/ISoundcloudResolver.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/NadekoBot/Modules/Music/Common/ISoundcloudResolver.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface ISoundcloudResolver : IPlatformQueryResolver
 | 
			
		||||
    {
 | 
			
		||||
        bool IsSoundCloudLink(string url);
 | 
			
		||||
        IAsyncEnumerable<ITrackInfo> ResolvePlaylistAsync(string playlist);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								src/NadekoBot/Modules/Music/Common/ITrackCacher.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/NadekoBot/Modules/Music/Common/ITrackCacher.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface ITrackCacher
 | 
			
		||||
    {
 | 
			
		||||
        Task<string?> GetOrCreateStreamLink(
 | 
			
		||||
            string id,
 | 
			
		||||
            MusicPlatform platform,
 | 
			
		||||
            Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Task CacheTrackDataAsync(ICachableTrackData data);
 | 
			
		||||
        Task<ICachableTrackData?> GetCachedDataByIdAsync(string id, MusicPlatform platform);
 | 
			
		||||
        Task<ICachableTrackData?> GetCachedDataByQueryAsync(string query, MusicPlatform platform);
 | 
			
		||||
        Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data);
 | 
			
		||||
        Task CacheStreamUrlAsync(string id, MusicPlatform platform, string url, TimeSpan expiry);
 | 
			
		||||
        Task<IReadOnlyCollection<string>> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform);
 | 
			
		||||
        Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable<string> ids);
 | 
			
		||||
        Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId);
 | 
			
		||||
        Task<string?> GetPlaylistIdByQueryAsync(string query, MusicPlatform platform);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								src/NadekoBot/Modules/Music/Common/ITrackInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/NadekoBot/Modules/Music/Common/ITrackInfo.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface ITrackInfo
 | 
			
		||||
    {
 | 
			
		||||
        public string Title { get; }
 | 
			
		||||
        public string Url { get; }
 | 
			
		||||
        public string Thumbnail { get; }
 | 
			
		||||
        public TimeSpan Duration { get; }
 | 
			
		||||
        public MusicPlatform Platform { get; }
 | 
			
		||||
        public ValueTask<string?> GetStreamUrl();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/NadekoBot/Modules/Music/Common/ITrackResolveProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/NadekoBot/Modules/Music/Common/ITrackResolveProvider.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface ITrackResolveProvider
 | 
			
		||||
    {
 | 
			
		||||
        Task<ITrackInfo?> QuerySongAsync(string query, MusicPlatform? forcePlatform);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/NadekoBot/Modules/Music/Common/IVoiceProxy.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/NadekoBot/Modules/Music/Common/IVoiceProxy.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Ayu.Discord.Voice;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface IVoiceProxy
 | 
			
		||||
    {
 | 
			
		||||
        VoiceProxy.VoiceProxyState State { get; }
 | 
			
		||||
        public bool SendPcmFrame(VoiceClient vc, Span<byte> data, int length);
 | 
			
		||||
        public void SetGateway(VoiceGateway gateway);
 | 
			
		||||
        Task StartSpeakingAsync();
 | 
			
		||||
        Task StopSpeakingAsync();
 | 
			
		||||
        public Task StartGateway();
 | 
			
		||||
        Task StopGateway();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/NadekoBot/Modules/Music/Common/IYoutubeResolver.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/NadekoBot/Modules/Music/Common/IYoutubeResolver.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public interface IYoutubeResolver : IPlatformQueryResolver
 | 
			
		||||
    {
 | 
			
		||||
        public Regex YtVideoIdRegex { get; }
 | 
			
		||||
        public Task<ITrackInfo?> ResolveByIdAsync(string id);
 | 
			
		||||
        IAsyncEnumerable<ITrackInfo> ResolveTracksFromPlaylistAsync(string query);
 | 
			
		||||
        Task<ITrackInfo?> ResolveByQueryAsync(string query, bool tryExtractingId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/NadekoBot/Modules/Music/Common/Impl/CachableTrackData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/NadekoBot/Modules/Music/Common/Impl/CachableTrackData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public sealed class CachableTrackData : ICachableTrackData
 | 
			
		||||
    {
 | 
			
		||||
        public string Title { get; set; } = string.Empty;
 | 
			
		||||
        public string Id { get; set; } = string.Empty;
 | 
			
		||||
        public string Url { get; set; } = string.Empty;
 | 
			
		||||
        public string Thumbnail { get; set; } = string.Empty;
 | 
			
		||||
        public double TotalDurationMs { get; set; }
 | 
			
		||||
        [JsonIgnore]
 | 
			
		||||
        public TimeSpan Duration => TimeSpan.FromMilliseconds(TotalDurationMs);
 | 
			
		||||
        public MusicPlatform Platform { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										92
									
								
								src/NadekoBot/Modules/Music/Common/Impl/MultimediaTimer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/NadekoBot/Modules/Music/Common/Impl/MultimediaTimer.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music.Common
 | 
			
		||||
{
 | 
			
		||||
    public sealed class MultimediaTimer : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        private delegate void LpTimeProcDelegate(uint uTimerID, uint uMsg, int dwUser, int dw1, int dw2);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The timeSetEvent function starts a specified timer event. The multimedia timer runs in its own thread.
 | 
			
		||||
        /// After the event is activated, it calls the specified callback function or sets or pulses the specified
 | 
			
		||||
        /// event object.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="uDelay">
 | 
			
		||||
        /// Event delay, in milliseconds. If this value is not in the range of the minimum and
 | 
			
		||||
        /// maximum event delays supported by the timer, the function returns an error.
 | 
			
		||||
        /// </param>
 | 
			
		||||
        /// <param name="uResolution">
 | 
			
		||||
        /// Resolution of the timer event, in milliseconds. The resolution increases with
 | 
			
		||||
        /// smaller values; a resolution of 0 indicates periodic events should occur with the greatest possible accuracy.
 | 
			
		||||
        /// To reduce system overhead, however, you should use the maximum value appropriate for your application.
 | 
			
		||||
        /// </param>
 | 
			
		||||
        /// <param name="lpTimeProc">
 | 
			
		||||
        /// Pointer to a callback function that is called once upon expiration of a single event or periodically upon
 | 
			
		||||
        /// expiration of periodic events. If fuEvent specifies the TIME_CALLBACK_EVENT_SET or TIME_CALLBACK_EVENT_PULSE
 | 
			
		||||
        /// flag, then the lpTimeProc parameter is interpreted as a handle to an event object. The event will be set or
 | 
			
		||||
        /// pulsed upon completion of a single event or periodically upon completion of periodic events.
 | 
			
		||||
        /// For any other value of fuEvent, the lpTimeProc parameter is a pointer to a callback function of type
 | 
			
		||||
        /// LPTIMECALLBACK.
 | 
			
		||||
        /// </param>
 | 
			
		||||
        /// <param name="dwUser">User-supplied callback data.</param>
 | 
			
		||||
        /// <param name="fuEvent"></param>
 | 
			
		||||
        /// <returns>Timer event type. This parameter may include one of the following values.</returns>
 | 
			
		||||
        [DllImport("Winmm.dll")]
 | 
			
		||||
        private static extern uint timeSetEvent(
 | 
			
		||||
            uint uDelay,
 | 
			
		||||
            uint uResolution,
 | 
			
		||||
            LpTimeProcDelegate lpTimeProc,
 | 
			
		||||
            int dwUser,
 | 
			
		||||
            TimerMode fuEvent
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        private enum TimerMode
 | 
			
		||||
        {
 | 
			
		||||
            OneShot,
 | 
			
		||||
            Periodic,
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The timeKillEvent function cancels a specified timer event.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="uTimerID">
 | 
			
		||||
        /// Identifier of the timer event to cancel.
 | 
			
		||||
        /// This identifier was returned by the timeSetEvent function when the timer event was set up.
 | 
			
		||||
        /// </param>
 | 
			
		||||
        /// <returns>Returns TIMERR_NOERROR if successful or MMSYSERR_INVALPARAM if the specified timer event does not exist.</returns>
 | 
			
		||||
        [DllImport("Winmm.dll")]
 | 
			
		||||
        private static extern int timeKillEvent(
 | 
			
		||||
            uint uTimerID
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        private LpTimeProcDelegate _lpTimeProc;
 | 
			
		||||
        private readonly uint _eventId;
 | 
			
		||||
        private readonly Action<object> _callback;
 | 
			
		||||
        private readonly object _state;
 | 
			
		||||
 | 
			
		||||
        public MultimediaTimer(Action<object> callback, object state, int period)
 | 
			
		||||
        {
 | 
			
		||||
            if (period <= 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0");
 | 
			
		||||
 | 
			
		||||
            _callback = callback;
 | 
			
		||||
            _state = state;
 | 
			
		||||
 | 
			
		||||
            _lpTimeProc = CallbackInternal;
 | 
			
		||||
            _eventId = timeSetEvent((uint)period, 1, _lpTimeProc, 0, TimerMode.Periodic);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void CallbackInternal(uint uTimerId, uint uMsg, int dwUser, int dw1, int dw2)
 | 
			
		||||
        {
 | 
			
		||||
            _callback(_state);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            _lpTimeProc = default;
 | 
			
		||||
            timeKillEvent(_eventId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								src/NadekoBot/Modules/Music/Common/Impl/MusicExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/NadekoBot/Modules/Music/Common/Impl/MusicExtensions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public static class MusicExtensions
 | 
			
		||||
    {
 | 
			
		||||
        public static string PrettyTotalTime(this IMusicPlayer mp)
 | 
			
		||||
        {
 | 
			
		||||
            long sum = 0;
 | 
			
		||||
            foreach (var track in mp.GetQueuedTracks())
 | 
			
		||||
            {
 | 
			
		||||
                if (track.Duration == TimeSpan.MaxValue)
 | 
			
		||||
                    return "∞";
 | 
			
		||||
 | 
			
		||||
                sum += track.Duration.Ticks;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var total = new TimeSpan(sum);
 | 
			
		||||
 | 
			
		||||
            return total.ToString(@"hh\:mm\:ss");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static string PrettyVolume(this IMusicPlayer mp)
 | 
			
		||||
            => $"🔉 {(int) (mp.Volume * 100)}%";
 | 
			
		||||
        
 | 
			
		||||
        public static string PrettyName(this ITrackInfo trackInfo)
 | 
			
		||||
            => $"**[{trackInfo.Title.TrimTo(60).Replace("[", "\\[").Replace("]", "\\]")}]({trackInfo.Url})**";
 | 
			
		||||
 | 
			
		||||
        public static string PrettyInfo(this IQueuedTrackInfo trackInfo)
 | 
			
		||||
            => $"{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {trackInfo.Queuer}";
 | 
			
		||||
 | 
			
		||||
        public static string PrettyFullName(this IQueuedTrackInfo trackInfo)
 | 
			
		||||
            => $@"{trackInfo.PrettyName()}
 | 
			
		||||
		`{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {Format.Sanitize(trackInfo.Queuer.TrimTo(15))}`";
 | 
			
		||||
 | 
			
		||||
        public static string PrettyTotalTime(this ITrackInfo trackInfo)
 | 
			
		||||
        {
 | 
			
		||||
            if (trackInfo.Duration == TimeSpan.Zero)
 | 
			
		||||
                return "(?)";
 | 
			
		||||
            if (trackInfo.Duration == TimeSpan.MaxValue)
 | 
			
		||||
                return "∞";
 | 
			
		||||
            if (trackInfo.Duration.TotalHours >= 1)
 | 
			
		||||
                return trackInfo.Duration.ToString(@"hh\:mm\:ss");
 | 
			
		||||
 | 
			
		||||
            return trackInfo.Duration.ToString(@"mm\:ss");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static ICachableTrackData ToCachedData(this ITrackInfo trackInfo, string id)
 | 
			
		||||
            => new CachableTrackData()
 | 
			
		||||
            {
 | 
			
		||||
                TotalDurationMs = trackInfo.Duration.TotalMilliseconds,
 | 
			
		||||
                Id = id,
 | 
			
		||||
                Thumbnail = trackInfo.Thumbnail,
 | 
			
		||||
                Url = trackInfo.Url,
 | 
			
		||||
                Platform = trackInfo.Platform,
 | 
			
		||||
                Title = trackInfo.Title
 | 
			
		||||
            };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/NadekoBot/Modules/Music/Common/Impl/MusicPlatform.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/NadekoBot/Modules/Music/Common/Impl/MusicPlatform.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public enum MusicPlatform
 | 
			
		||||
    {
 | 
			
		||||
        Radio,
 | 
			
		||||
        Youtube,
 | 
			
		||||
        Local,
 | 
			
		||||
        SoundCloud,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										540
									
								
								src/NadekoBot/Modules/Music/Common/Impl/MusicPlayer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										540
									
								
								src/NadekoBot/Modules/Music/Common/Impl/MusicPlayer.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,540 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Ayu.Discord.Voice;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Music.Common;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Music;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public sealed class MusicPlayer : IMusicPlayer
 | 
			
		||||
    {
 | 
			
		||||
        private delegate void AdjustVolumeDelegate(Span<byte> data, float volume);
 | 
			
		||||
 | 
			
		||||
        private AdjustVolumeDelegate AdjustVolume;
 | 
			
		||||
        private readonly VoiceClient _vc;
 | 
			
		||||
 | 
			
		||||
        public bool IsKilled { get; private set; }
 | 
			
		||||
        public bool IsStopped { get; private set; }
 | 
			
		||||
        public bool IsPaused { get; private set; }
 | 
			
		||||
        public PlayerRepeatType Repeat { get; private set; }
 | 
			
		||||
        
 | 
			
		||||
        public int CurrentIndex => _queue.Index;
 | 
			
		||||
        
 | 
			
		||||
        public float Volume => _volume;
 | 
			
		||||
        private float _volume = 1.0f;
 | 
			
		||||
 | 
			
		||||
        private readonly IMusicQueue _queue;
 | 
			
		||||
        private readonly ITrackResolveProvider _trackResolveProvider;
 | 
			
		||||
        private readonly IVoiceProxy _proxy;
 | 
			
		||||
        private readonly ISongBuffer _songBuffer;
 | 
			
		||||
 | 
			
		||||
        private bool _skipped;
 | 
			
		||||
        private int? _forceIndex;
 | 
			
		||||
        private readonly Thread _thread;
 | 
			
		||||
        private readonly Random _rng;
 | 
			
		||||
 | 
			
		||||
        public MusicPlayer(
 | 
			
		||||
            IMusicQueue queue,
 | 
			
		||||
            ITrackResolveProvider trackResolveProvider,
 | 
			
		||||
            IVoiceProxy proxy,
 | 
			
		||||
            QualityPreset qualityPreset)
 | 
			
		||||
        {
 | 
			
		||||
            _queue = queue;
 | 
			
		||||
            _trackResolveProvider = trackResolveProvider;
 | 
			
		||||
            _proxy = proxy;
 | 
			
		||||
            _rng = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
            _vc = GetVoiceClient(qualityPreset);
 | 
			
		||||
            if (_vc.BitDepth == 16)
 | 
			
		||||
                AdjustVolume = AdjustVolumeInt16;
 | 
			
		||||
            else
 | 
			
		||||
                AdjustVolume = AdjustVolumeFloat32;
 | 
			
		||||
            
 | 
			
		||||
            _songBuffer = new PoopyBufferImmortalized(_vc.InputLength);
 | 
			
		||||
 | 
			
		||||
            _thread = new Thread(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                await PlayLoop();
 | 
			
		||||
            });
 | 
			
		||||
            _thread.Start();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static VoiceClient GetVoiceClient(QualityPreset qualityPreset)
 | 
			
		||||
            => qualityPreset switch
 | 
			
		||||
            {
 | 
			
		||||
                QualityPreset.Highest => new VoiceClient(
 | 
			
		||||
                    SampleRate._48k,
 | 
			
		||||
                    Bitrate._192k,
 | 
			
		||||
                    Channels.Two,
 | 
			
		||||
                    FrameDelay.Delay20,
 | 
			
		||||
                    BitDepthEnum.Float32
 | 
			
		||||
                ),
 | 
			
		||||
                QualityPreset.High => new VoiceClient(
 | 
			
		||||
                    SampleRate._48k,
 | 
			
		||||
                    Bitrate._128k,
 | 
			
		||||
                    Channels.Two,
 | 
			
		||||
                    FrameDelay.Delay40,
 | 
			
		||||
                    BitDepthEnum.Float32
 | 
			
		||||
                ),
 | 
			
		||||
                QualityPreset.Medium => new VoiceClient(
 | 
			
		||||
                    SampleRate._48k,
 | 
			
		||||
                    Bitrate._96k,
 | 
			
		||||
                    Channels.Two,
 | 
			
		||||
                    FrameDelay.Delay40,
 | 
			
		||||
                    BitDepthEnum.UInt16
 | 
			
		||||
                ),
 | 
			
		||||
                QualityPreset.Low => new VoiceClient(
 | 
			
		||||
                    SampleRate._48k,
 | 
			
		||||
                    Bitrate._64k,
 | 
			
		||||
                    Channels.Two,
 | 
			
		||||
                    FrameDelay.Delay40,
 | 
			
		||||
                    BitDepthEnum.UInt16
 | 
			
		||||
                ),
 | 
			
		||||
                _ => throw new ArgumentOutOfRangeException(nameof(qualityPreset), qualityPreset, null)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        private async Task PlayLoop()
 | 
			
		||||
        {
 | 
			
		||||
            var sw = new Stopwatch();
 | 
			
		||||
 | 
			
		||||
            while (!IsKilled)
 | 
			
		||||
            {
 | 
			
		||||
                // wait until a song is available in the queue
 | 
			
		||||
                // or until the queue is resumed
 | 
			
		||||
                var track = _queue.GetCurrent(out int index);
 | 
			
		||||
                
 | 
			
		||||
                if (track is null || IsStopped)
 | 
			
		||||
                {
 | 
			
		||||
                    await Task.Delay(500);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (_skipped)
 | 
			
		||||
                {
 | 
			
		||||
                    _skipped = false;
 | 
			
		||||
                    _queue.Advance();
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                using var cancellationTokenSource = new CancellationTokenSource();
 | 
			
		||||
                var token = cancellationTokenSource.Token;
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    // light up green in vc
 | 
			
		||||
                    _ = _proxy.StartSpeakingAsync();
 | 
			
		||||
 | 
			
		||||
                    _ = OnStarted?.Invoke(this, track, index);
 | 
			
		||||
 | 
			
		||||
                    // make sure song buffer is ready to be (re)used
 | 
			
		||||
                    _songBuffer.Reset();
 | 
			
		||||
 | 
			
		||||
                    var streamUrl = await track.GetStreamUrl();
 | 
			
		||||
                    // start up the data source
 | 
			
		||||
                    using var source = FfmpegTrackDataSource.CreateAsync(
 | 
			
		||||
                        _vc.BitDepth,
 | 
			
		||||
                        streamUrl,
 | 
			
		||||
                        track.Platform == MusicPlatform.Local
 | 
			
		||||
                    );
 | 
			
		||||
                    
 | 
			
		||||
                    // start moving data from the source into the buffer
 | 
			
		||||
                    // this method will return once the sufficient prebuffering is done
 | 
			
		||||
                    await _songBuffer.BufferAsync(source, token);
 | 
			
		||||
 | 
			
		||||
                    // // Implemenation with multimedia timer. Works but a hassle because no support for switching
 | 
			
		||||
                    // // vcs, as any error in copying will cancel the song. Also no idea how to use this as an option
 | 
			
		||||
                    // // for selfhosters.
 | 
			
		||||
                    // if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 | 
			
		||||
                    // {
 | 
			
		||||
                    //     var cancelSource = new CancellationTokenSource();
 | 
			
		||||
                    //     var cancelToken = cancelSource.Token;
 | 
			
		||||
                    //     using var timer = new MultimediaTimer(_ =>
 | 
			
		||||
                    //     {
 | 
			
		||||
                    //         if (IsStopped || IsKilled)
 | 
			
		||||
                    //         {
 | 
			
		||||
                    //             cancelSource.Cancel();
 | 
			
		||||
                    //             return;
 | 
			
		||||
                    //         }
 | 
			
		||||
                    //         
 | 
			
		||||
                    //         if (_skipped)
 | 
			
		||||
                    //         {
 | 
			
		||||
                    //             _skipped = false;
 | 
			
		||||
                    //             cancelSource.Cancel();
 | 
			
		||||
                    //             return;
 | 
			
		||||
                    //         }
 | 
			
		||||
                    //
 | 
			
		||||
                    //         if (IsPaused)
 | 
			
		||||
                    //             return;
 | 
			
		||||
                    //
 | 
			
		||||
                    //         try
 | 
			
		||||
                    //         {
 | 
			
		||||
                    //             // this should tolerate certain number of errors
 | 
			
		||||
                    //             var result = CopyChunkToOutput(_songBuffer, _vc);
 | 
			
		||||
                    //             if (!result)
 | 
			
		||||
                    //                 cancelSource.Cancel();
 | 
			
		||||
                    //               
 | 
			
		||||
                    //         }
 | 
			
		||||
                    //         catch (Exception ex)
 | 
			
		||||
                    //         {
 | 
			
		||||
                    //             Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message);
 | 
			
		||||
                    //             cancelSource.Cancel();
 | 
			
		||||
                    //         }
 | 
			
		||||
                    //
 | 
			
		||||
                    //     }, null, 20);
 | 
			
		||||
                    //     
 | 
			
		||||
                    //     while(true)
 | 
			
		||||
                    //         await Task.Delay(1000, cancelToken);
 | 
			
		||||
                    // }
 | 
			
		||||
 | 
			
		||||
                    // start sending data
 | 
			
		||||
                    var ticksPerMs = 1000f / Stopwatch.Frequency;
 | 
			
		||||
                    sw.Start();
 | 
			
		||||
                    Thread.Sleep(2);
 | 
			
		||||
 | 
			
		||||
                    var delay = sw.ElapsedTicks * ticksPerMs > 3f
 | 
			
		||||
                        ? _vc.Delay - 16
 | 
			
		||||
                        : _vc.Delay - 3;
 | 
			
		||||
                    
 | 
			
		||||
                    var errorCount = 0;
 | 
			
		||||
                    while (!IsStopped && !IsKilled)
 | 
			
		||||
                    {
 | 
			
		||||
                        // doing the skip this way instead of in the condition
 | 
			
		||||
                        // ensures that a song will for sure be skipped
 | 
			
		||||
                        if (_skipped)
 | 
			
		||||
                        {
 | 
			
		||||
                            _skipped = false;
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                        if (IsPaused)
 | 
			
		||||
                        {
 | 
			
		||||
                            await Task.Delay(200);
 | 
			
		||||
                            continue;
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                        sw.Restart();
 | 
			
		||||
                        var ticks = sw.ElapsedTicks;
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            var result = CopyChunkToOutput(_songBuffer, _vc);
 | 
			
		||||
                        
 | 
			
		||||
                            // if song is finished
 | 
			
		||||
                            if (result is null)
 | 
			
		||||
                                break;
 | 
			
		||||
                        
 | 
			
		||||
                            if (result is true)
 | 
			
		||||
                            {
 | 
			
		||||
                                if (errorCount > 0)
 | 
			
		||||
                                {
 | 
			
		||||
                                    _ = _proxy.StartSpeakingAsync();
 | 
			
		||||
                                    errorCount = 0;
 | 
			
		||||
                                }
 | 
			
		||||
                                    
 | 
			
		||||
                                // todo future windows multimedia api
 | 
			
		||||
                                    
 | 
			
		||||
                                // wait for slightly less than the latency
 | 
			
		||||
                                Thread.Sleep(delay);
 | 
			
		||||
                                    
 | 
			
		||||
                                // and then spin out the rest
 | 
			
		||||
                                while ((sw.ElapsedTicks - ticks) * ticksPerMs <= _vc.Delay - 0.1f)
 | 
			
		||||
                                    Thread.SpinWait(100);
 | 
			
		||||
                            }
 | 
			
		||||
                            else
 | 
			
		||||
                            {
 | 
			
		||||
                                // result is false is either when the gateway is being swapped 
 | 
			
		||||
                                // or if the bot is reconnecting, or just disconnected for whatever reason
 | 
			
		||||
                        
 | 
			
		||||
                                // tolerate up to 15x200ms of failures (3 seconds)
 | 
			
		||||
                                if (++errorCount <= 15)
 | 
			
		||||
                                {
 | 
			
		||||
                                    await Task.Delay(200);
 | 
			
		||||
                                    continue;
 | 
			
		||||
                                }
 | 
			
		||||
                        
 | 
			
		||||
                                Log.Warning("Can't send data to voice channel");
 | 
			
		||||
                        
 | 
			
		||||
                                IsStopped = true;
 | 
			
		||||
                                // if errors are happening for more than 3 seconds
 | 
			
		||||
                                // Stop the player
 | 
			
		||||
                                break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        catch (Exception ex)
 | 
			
		||||
                        {
 | 
			
		||||
                            Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Win32Exception)
 | 
			
		||||
                {
 | 
			
		||||
                    IsStopped = true;
 | 
			
		||||
                    Log.Error("Please install ffmpeg and make sure it's added to your " +
 | 
			
		||||
                              "PATH environment variable before trying again");
 | 
			
		||||
                }
 | 
			
		||||
                catch (OperationCanceledException)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Information("Song skipped");
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Error(ex, "Unknown error in music loop: {ErrorMessage}", ex.Message);
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    cancellationTokenSource.Cancel();
 | 
			
		||||
                    // turn off green in vc
 | 
			
		||||
                    _ = OnCompleted?.Invoke(this, track);
 | 
			
		||||
                    
 | 
			
		||||
                    HandleQueuePostTrack();
 | 
			
		||||
                    _skipped = false;
 | 
			
		||||
                    
 | 
			
		||||
                    _ = _proxy.StopSpeakingAsync();;
 | 
			
		||||
                    
 | 
			
		||||
                    await Task.Delay(100);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private bool? CopyChunkToOutput(ISongBuffer sb, VoiceClient vc)
 | 
			
		||||
        {
 | 
			
		||||
            var data = sb.Read(vc.InputLength, out var length);
 | 
			
		||||
 | 
			
		||||
            // if nothing is read from the buffer, song is finished
 | 
			
		||||
            if (data.Length == 0)
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            AdjustVolume(data, _volume);
 | 
			
		||||
            return _proxy.SendPcmFrame(vc, data, length);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void HandleQueuePostTrack()
 | 
			
		||||
        {
 | 
			
		||||
            if (_forceIndex is int forceIndex)
 | 
			
		||||
            {
 | 
			
		||||
                _queue.SetIndex(forceIndex);
 | 
			
		||||
                _forceIndex = null;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var (repeat, isStopped) = (Repeat, IsStopped);
 | 
			
		||||
 | 
			
		||||
            if (repeat == PlayerRepeatType.Track || isStopped)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            // if queue is being repeated, advance no matter what
 | 
			
		||||
            if (repeat == PlayerRepeatType.None)
 | 
			
		||||
            {
 | 
			
		||||
                // if this is the last song,
 | 
			
		||||
                // stop the queue
 | 
			
		||||
                if (_queue.IsLast())
 | 
			
		||||
                {
 | 
			
		||||
                    IsStopped = true;
 | 
			
		||||
                    OnQueueStopped?.Invoke(this);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                _queue.Advance();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            _queue.Advance();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        private static void AdjustVolumeInt16(Span<byte> audioSamples, float volume)
 | 
			
		||||
        {
 | 
			
		||||
            if (Math.Abs(volume - 1f) < 0.0001f) return;
 | 
			
		||||
        
 | 
			
		||||
            var samples = MemoryMarshal.Cast<byte, short>(audioSamples);
 | 
			
		||||
        
 | 
			
		||||
            for (var i = 0; i < samples.Length; i++)
 | 
			
		||||
            {
 | 
			
		||||
                ref var sample = ref samples[i];
 | 
			
		||||
                sample = (short) (sample * volume);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        private static void AdjustVolumeFloat32(Span<byte> audioSamples, float volume)
 | 
			
		||||
        {
 | 
			
		||||
            if (Math.Abs(volume - 1f) < 0.0001f) return;
 | 
			
		||||
        
 | 
			
		||||
            var samples = MemoryMarshal.Cast<byte, float>(audioSamples);
 | 
			
		||||
        
 | 
			
		||||
            for (var i = 0; i < samples.Length; i++)
 | 
			
		||||
            {
 | 
			
		||||
                ref var sample = ref samples[i];
 | 
			
		||||
                sample = (float) (sample * volume);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync(
 | 
			
		||||
            string query, 
 | 
			
		||||
            string queuer,
 | 
			
		||||
            bool asNext,
 | 
			
		||||
            MusicPlatform? forcePlatform = null)
 | 
			
		||||
        {
 | 
			
		||||
            var song = await _trackResolveProvider.QuerySongAsync(query, forcePlatform);
 | 
			
		||||
            if (song is null)
 | 
			
		||||
                return default;
 | 
			
		||||
 | 
			
		||||
            int index;
 | 
			
		||||
 | 
			
		||||
            if (asNext)
 | 
			
		||||
                return (_queue.EnqueueNext(song, queuer, out index), index);
 | 
			
		||||
 | 
			
		||||
            return (_queue.Enqueue(song, queuer, out index), index);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public async Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer)
 | 
			
		||||
        {
 | 
			
		||||
            var errorCount = 0;
 | 
			
		||||
            foreach (var chunk in queries.Chunk(5))
 | 
			
		||||
            {
 | 
			
		||||
                if (IsKilled)
 | 
			
		||||
                    break;
 | 
			
		||||
                
 | 
			
		||||
                var queueTasks = chunk.Select(async data =>
 | 
			
		||||
                {
 | 
			
		||||
                    var (query, platform) = data;
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await TryEnqueueTrackAsync(query, queuer, false, forcePlatform: platform).ConfigureAwait(false);
 | 
			
		||||
                        errorCount = 0;
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query);
 | 
			
		||||
                        ++errorCount;
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                await Task.WhenAll(queueTasks);
 | 
			
		||||
                await Task.Delay(1000);
 | 
			
		||||
                
 | 
			
		||||
                // > 10 errors in a row = kill
 | 
			
		||||
                if (errorCount > 10)
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public void EnqueueTrack(ITrackInfo track, string queuer)
 | 
			
		||||
        {
 | 
			
		||||
            _queue.Enqueue(track, queuer, out _);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void EnqueueTracks(IEnumerable<ITrackInfo> tracks, string queuer)
 | 
			
		||||
        {
 | 
			
		||||
            _queue.EnqueueMany(tracks, queuer);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void SetRepeat(PlayerRepeatType type)
 | 
			
		||||
        {
 | 
			
		||||
            Repeat = type;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void ShuffleQueue()
 | 
			
		||||
        {
 | 
			
		||||
            _queue.Shuffle(_rng);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Stop()
 | 
			
		||||
        {
 | 
			
		||||
            IsStopped = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Clear()
 | 
			
		||||
        {
 | 
			
		||||
            _queue.Clear();
 | 
			
		||||
            _skipped = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IReadOnlyCollection<IQueuedTrackInfo> GetQueuedTracks()
 | 
			
		||||
            => _queue.List();
 | 
			
		||||
 | 
			
		||||
        public IQueuedTrackInfo? GetCurrentTrack(out int index)
 | 
			
		||||
            => _queue.GetCurrent(out index);
 | 
			
		||||
 | 
			
		||||
        public void Next()
 | 
			
		||||
        {
 | 
			
		||||
            _skipped = true;
 | 
			
		||||
            IsStopped = false;
 | 
			
		||||
            IsPaused = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool MoveTo(int index)
 | 
			
		||||
        {
 | 
			
		||||
            if (_queue.SetIndex(index))
 | 
			
		||||
            {
 | 
			
		||||
                _forceIndex = index;
 | 
			
		||||
                _skipped = true;
 | 
			
		||||
                IsStopped = false;
 | 
			
		||||
                IsPaused = false;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void SetVolume(int newVolume)
 | 
			
		||||
        {
 | 
			
		||||
            var normalizedVolume = newVolume / 100f;
 | 
			
		||||
            if (normalizedVolume < 0f || normalizedVolume > 1f)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(newVolume), "Volume must be in range 0-100");
 | 
			
		||||
 | 
			
		||||
            _volume = normalizedVolume;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Kill()
 | 
			
		||||
        {
 | 
			
		||||
            IsKilled = true;
 | 
			
		||||
            IsStopped = true;
 | 
			
		||||
            IsPaused = false;
 | 
			
		||||
            _skipped = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_queue.TryRemoveAt(index, out trackInfo, out var isCurrent))
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            if (isCurrent)
 | 
			
		||||
                _skipped = true;
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public bool TogglePause() => IsPaused = !IsPaused;
 | 
			
		||||
        public IQueuedTrackInfo? MoveTrack(int from, int to) => _queue.MoveTrack(from, to);
 | 
			
		||||
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            IsKilled = true;
 | 
			
		||||
            OnCompleted = null;
 | 
			
		||||
            OnStarted = null;
 | 
			
		||||
            OnQueueStopped = null;
 | 
			
		||||
            _queue.Clear();
 | 
			
		||||
            _songBuffer.Dispose();
 | 
			
		||||
            _vc.Dispose();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public event Func<IMusicPlayer, IQueuedTrackInfo, Task>? OnCompleted;
 | 
			
		||||
        public event Func<IMusicPlayer, IQueuedTrackInfo, int, Task>? OnStarted;
 | 
			
		||||
        public event Func<IMusicPlayer, Task>? OnQueueStopped;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										323
									
								
								src/NadekoBot/Modules/Music/Common/Impl/MusicQueue.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										323
									
								
								src/NadekoBot/Modules/Music/Common/Impl/MusicQueue.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,323 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public sealed partial class MusicQueue
 | 
			
		||||
    {
 | 
			
		||||
        private sealed class QueuedTrackInfo : IQueuedTrackInfo
 | 
			
		||||
        {
 | 
			
		||||
            public ITrackInfo TrackInfo { get; }
 | 
			
		||||
            public string Queuer { get; }
 | 
			
		||||
 | 
			
		||||
            public string Title => TrackInfo.Title;
 | 
			
		||||
            public string Url => TrackInfo.Url;
 | 
			
		||||
            public string Thumbnail => TrackInfo.Thumbnail;
 | 
			
		||||
            public TimeSpan Duration => TrackInfo.Duration;
 | 
			
		||||
            public MusicPlatform Platform => TrackInfo.Platform;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            public QueuedTrackInfo(ITrackInfo trackInfo, string queuer)
 | 
			
		||||
            {
 | 
			
		||||
                TrackInfo = trackInfo;
 | 
			
		||||
                Queuer = queuer;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public ValueTask<string?> GetStreamUrl() => TrackInfo.GetStreamUrl();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public sealed partial class MusicQueue : IMusicQueue
 | 
			
		||||
    {
 | 
			
		||||
        private LinkedList<QueuedTrackInfo> _tracks;
 | 
			
		||||
 | 
			
		||||
        public int Index
 | 
			
		||||
        {
 | 
			
		||||
            get
 | 
			
		||||
            {
 | 
			
		||||
                // just make sure the internal logic runs first
 | 
			
		||||
                // to make sure that some potential indermediate value is not returned
 | 
			
		||||
                lock (locker)
 | 
			
		||||
                {
 | 
			
		||||
                    return _index;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private int _index;
 | 
			
		||||
 | 
			
		||||
        public int Count
 | 
			
		||||
        {
 | 
			
		||||
            get
 | 
			
		||||
            {
 | 
			
		||||
                lock (locker)
 | 
			
		||||
                {
 | 
			
		||||
                    return _tracks.Count;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly object locker = new object();
 | 
			
		||||
 | 
			
		||||
        public MusicQueue()
 | 
			
		||||
        {
 | 
			
		||||
            _index = 0;
 | 
			
		||||
            _tracks = new LinkedList<QueuedTrackInfo>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int index)
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                var added = new QueuedTrackInfo(trackInfo, queuer);
 | 
			
		||||
                index = _tracks.Count;
 | 
			
		||||
                _tracks.AddLast(added);
 | 
			
		||||
                return added;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IQueuedTrackInfo EnqueueNext(ITrackInfo trackInfo, string queuer, out int index)
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                if (_tracks.Count == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    return Enqueue(trackInfo, queuer, out index);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                LinkedListNode<QueuedTrackInfo> currentNode = _tracks.First!;
 | 
			
		||||
                int i;
 | 
			
		||||
                for (i = 1; i <= _index; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    currentNode = currentNode.Next!; // can't be null because index is always in range of the count
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var added = new QueuedTrackInfo(trackInfo, queuer);
 | 
			
		||||
                index = i;
 | 
			
		||||
 | 
			
		||||
                _tracks.AddAfter(currentNode, added);
 | 
			
		||||
 | 
			
		||||
                return added;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void EnqueueMany(IEnumerable<ITrackInfo> tracks, string queuer)
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var track in tracks)
 | 
			
		||||
                {
 | 
			
		||||
                    var added = new QueuedTrackInfo(track, queuer);
 | 
			
		||||
                    _tracks.AddLast(added);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IReadOnlyCollection<IQueuedTrackInfo> List()
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                return _tracks.ToList();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IQueuedTrackInfo? GetCurrent(out int index)
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                index = _index;
 | 
			
		||||
                return _tracks.ElementAtOrDefault(_index);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Advance()
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                if (++_index >= _tracks.Count)
 | 
			
		||||
                    _index = 0;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Clear()
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                _tracks.Clear();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool SetIndex(int index)
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                if (index < 0 || index >= _tracks.Count)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                _index = index;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void RemoveAtInternal(int index, out IQueuedTrackInfo trackInfo)
 | 
			
		||||
        {
 | 
			
		||||
            var removedNode = _tracks.First!;
 | 
			
		||||
            int i;
 | 
			
		||||
            for (i = 0; i < index; i++)
 | 
			
		||||
            {
 | 
			
		||||
                removedNode = removedNode.Next!;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            trackInfo = removedNode.Value;
 | 
			
		||||
            _tracks.Remove(removedNode);
 | 
			
		||||
 | 
			
		||||
            if (i <= _index)
 | 
			
		||||
                --_index;
 | 
			
		||||
 | 
			
		||||
            if (_index < 0)
 | 
			
		||||
                _index = Count;
 | 
			
		||||
 | 
			
		||||
            // if it was the last song in the queue
 | 
			
		||||
            // // wrap back to start
 | 
			
		||||
            // if (_index == Count)
 | 
			
		||||
            //     _index = 0;
 | 
			
		||||
            // else if (i <= _index)
 | 
			
		||||
            //     if (_index == 0)
 | 
			
		||||
            //         _index = Count;
 | 
			
		||||
            //     else --_index;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void RemoveCurrent()
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                if (_index < _tracks.Count)
 | 
			
		||||
                    RemoveAtInternal(_index, out _);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IQueuedTrackInfo? MoveTrack(int from, int to)
 | 
			
		||||
        {
 | 
			
		||||
            if (from < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(from));
 | 
			
		||||
            if (to < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(to));
 | 
			
		||||
            if (to == from)
 | 
			
		||||
                throw new ArgumentException($"{nameof(from)} and {nameof(to)} must be different");
 | 
			
		||||
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                if (from >= Count || to >= Count)
 | 
			
		||||
                    return null;
 | 
			
		||||
 | 
			
		||||
                // update current track index
 | 
			
		||||
                if (from == _index)
 | 
			
		||||
                {
 | 
			
		||||
                    // if the song being moved is the current track
 | 
			
		||||
                    // it means that it will for sure end up on the destination
 | 
			
		||||
                    _index = to;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // moving a track from below the current track means 
 | 
			
		||||
                    // means it will drop down
 | 
			
		||||
                    if (from < _index)
 | 
			
		||||
                        _index--;
 | 
			
		||||
 | 
			
		||||
                    // moving a track to below the current track
 | 
			
		||||
                    // means it will rise up
 | 
			
		||||
                    if (to <= _index)
 | 
			
		||||
                        _index++;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    // if both from and to are below _index - net change is + 1 - 1 = 0
 | 
			
		||||
                    // if from is below and to is above - net change is -1 (as the track is taken and put above)
 | 
			
		||||
                    // if from is above and to is below - net change is 1 (as the track is inserted under)
 | 
			
		||||
                    // if from is above and to is above - net change is 0
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // get the node which needs to be moved
 | 
			
		||||
                var fromNode = _tracks.First!;
 | 
			
		||||
                for (var i = 0; i < from; i++)
 | 
			
		||||
                    fromNode = fromNode.Next!;
 | 
			
		||||
 | 
			
		||||
                // remove it from the queue
 | 
			
		||||
                _tracks.Remove(fromNode);
 | 
			
		||||
 | 
			
		||||
                // if it needs to be added as a first node,
 | 
			
		||||
                // add it directly and return
 | 
			
		||||
                if (to == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    _tracks.AddFirst(fromNode);
 | 
			
		||||
                    return fromNode.Value;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // else find the node at the index before the specified target
 | 
			
		||||
                var addAfterNode = _tracks.First!;
 | 
			
		||||
                for (var i = 1; i < to; i++)
 | 
			
		||||
                    addAfterNode = addAfterNode.Next!;
 | 
			
		||||
 | 
			
		||||
                // and add after it
 | 
			
		||||
                _tracks.AddAfter(addAfterNode, fromNode);
 | 
			
		||||
                return fromNode.Value;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Shuffle(Random rng)
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                var list = _tracks.ToList();
 | 
			
		||||
 | 
			
		||||
                for (var i = 0; i < list.Count; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    var struck = rng.Next(i, list.Count);
 | 
			
		||||
                    var temp = list[struck];
 | 
			
		||||
                    list[struck] = list[i];
 | 
			
		||||
                    list[i] = temp;
 | 
			
		||||
 | 
			
		||||
                    // could preserving the index during shuffling be done better?
 | 
			
		||||
                    if (i == _index)
 | 
			
		||||
                        _index = struck;
 | 
			
		||||
                    else if (struck == _index)
 | 
			
		||||
                        _index = i;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                _tracks = new LinkedList<QueuedTrackInfo>(list);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool IsLast()
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                return _index == _tracks.Count // if there are no tracks
 | 
			
		||||
                       || _index == _tracks.Count - 1;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool TryRemoveAt(int index, out IQueuedTrackInfo? trackInfo, out bool isCurrent)
 | 
			
		||||
        {
 | 
			
		||||
            lock (locker)
 | 
			
		||||
            {
 | 
			
		||||
                isCurrent = false;
 | 
			
		||||
                trackInfo = null;
 | 
			
		||||
 | 
			
		||||
                if (index < 0 || index >= _tracks.Count)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (index == _index)
 | 
			
		||||
                {
 | 
			
		||||
                    isCurrent = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                RemoveAtInternal(index, out trackInfo);
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										213
									
								
								src/NadekoBot/Modules/Music/Common/Impl/RedisTrackCacher.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								src/NadekoBot/Modules/Music/Common/Impl/RedisTrackCacher.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,213 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Serilog;
 | 
			
		||||
using StackExchange.Redis;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public sealed class RedisTrackCacher : ITrackCacher
 | 
			
		||||
    {
 | 
			
		||||
        private readonly ConnectionMultiplexer _multiplexer;
 | 
			
		||||
 | 
			
		||||
        public RedisTrackCacher(ConnectionMultiplexer multiplexer)
 | 
			
		||||
        {
 | 
			
		||||
            _multiplexer = multiplexer;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public async Task<string?> GetOrCreateStreamLink(
 | 
			
		||||
            string id,
 | 
			
		||||
            MusicPlatform platform,
 | 
			
		||||
            Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory
 | 
			
		||||
        )
 | 
			
		||||
        {
 | 
			
		||||
            var trackStreamKey = CreateStreamKey(id, platform);
 | 
			
		||||
            
 | 
			
		||||
            var value = await GetStreamFromCacheInternalAsync(trackStreamKey);
 | 
			
		||||
            
 | 
			
		||||
            // if there is no cached value
 | 
			
		||||
            if (value == default)
 | 
			
		||||
            {
 | 
			
		||||
                // otherwise retrieve and cache a new value, and run this method again
 | 
			
		||||
                var success = await CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory);
 | 
			
		||||
                if (!success)
 | 
			
		||||
                    return null;
 | 
			
		||||
                
 | 
			
		||||
                return await GetOrCreateStreamLink(id, platform, streamUrlFactory);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // cache new one for future use
 | 
			
		||||
            _ = Task.Run(() => CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory));
 | 
			
		||||
            
 | 
			
		||||
            return value;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        private static string CreateStreamKey(string id, MusicPlatform platform)
 | 
			
		||||
            => $"track:stream:{platform}:{id}";
 | 
			
		||||
 | 
			
		||||
        private async Task<bool> CreateAndCacheStreamUrlAsync(
 | 
			
		||||
            string trackStreamKey,
 | 
			
		||||
            Func<Task<(string StreamUrl, TimeSpan Expiry)>> factory)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var data = await factory();
 | 
			
		||||
                if (data == default)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                await CacheStreamUrlInternalAsync(trackStreamKey, data.StreamUrl, data.Expiry);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Error resolving stream link for {TrackCacheKey}", trackStreamKey);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task CacheStreamUrlAsync(string id, MusicPlatform platform, string url, TimeSpan expiry)
 | 
			
		||||
            => CacheStreamUrlInternalAsync(CreateStreamKey(id, platform), url, expiry);
 | 
			
		||||
 | 
			
		||||
        private async Task CacheStreamUrlInternalAsync(string trackStreamKey, string url, TimeSpan expiry)
 | 
			
		||||
        {
 | 
			
		||||
            // keys need to be expired after an hour
 | 
			
		||||
            // to make sure client doesn't get an expired stream url
 | 
			
		||||
            // to achieve this, track keys will be just pointers to real data
 | 
			
		||||
            // but that data will expire
 | 
			
		||||
            
 | 
			
		||||
            var db = _multiplexer.GetDatabase();
 | 
			
		||||
            var dataKey = $"entry:{Guid.NewGuid()}:{trackStreamKey}";
 | 
			
		||||
            await db.StringSetAsync(dataKey, url, expiry: expiry);
 | 
			
		||||
            await db.ListRightPushAsync(trackStreamKey, dataKey);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<string?> GetStreamFromCacheInternalAsync(string trackStreamKey)
 | 
			
		||||
        {
 | 
			
		||||
            // Job of the method which retrieves keys is to pop the elements
 | 
			
		||||
            // from the list of cached trackurls until it finds a non-expired key
 | 
			
		||||
 | 
			
		||||
            var db = _multiplexer.GetDatabase();
 | 
			
		||||
            while(true)
 | 
			
		||||
            {
 | 
			
		||||
                string? dataKey = await db.ListLeftPopAsync(trackStreamKey);
 | 
			
		||||
                if (dataKey == default)
 | 
			
		||||
                    return null;
 | 
			
		||||
 | 
			
		||||
                var streamUrl = await db.StringGetAsync(dataKey);
 | 
			
		||||
                if (streamUrl == default)
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                return streamUrl;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        private static string CreateCachedDataKey(string id, MusicPlatform platform)
 | 
			
		||||
            => $"track:data:{platform}:{id}";
 | 
			
		||||
        
 | 
			
		||||
        public Task CacheTrackDataAsync(ICachableTrackData data)
 | 
			
		||||
        {
 | 
			
		||||
            var db = _multiplexer.GetDatabase();
 | 
			
		||||
 | 
			
		||||
            var trackDataKey = CreateCachedDataKey(data.Id, data.Platform);
 | 
			
		||||
            var dataString = JsonSerializer.Serialize((object)data);
 | 
			
		||||
            // cache for 1 day
 | 
			
		||||
            return db.StringSetAsync(trackDataKey, dataString, expiry: TimeSpan.FromDays(1));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<ICachableTrackData?> GetCachedDataByIdAsync(string id, MusicPlatform platform)
 | 
			
		||||
        {
 | 
			
		||||
            var db = _multiplexer.GetDatabase();
 | 
			
		||||
            
 | 
			
		||||
            var trackDataKey = CreateCachedDataKey(id, platform);
 | 
			
		||||
            var data = await db.StringGetAsync(trackDataKey);
 | 
			
		||||
            if (data == default)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            return JsonSerializer.Deserialize<CachableTrackData>(data);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        private static string CreateCachedQueryDataKey(string query, MusicPlatform platform)
 | 
			
		||||
            => $"track:query_to_id:{platform}:{query}";
 | 
			
		||||
        public async Task<ICachableTrackData?> GetCachedDataByQueryAsync(string query, MusicPlatform platform)
 | 
			
		||||
        {
 | 
			
		||||
            query = Uri.EscapeDataString(query.Trim());
 | 
			
		||||
            
 | 
			
		||||
            var db = _multiplexer.GetDatabase();
 | 
			
		||||
            var queryDataKey = CreateCachedQueryDataKey(query, platform);
 | 
			
		||||
 | 
			
		||||
            var trackId = await db.StringGetAsync(queryDataKey);
 | 
			
		||||
            if (trackId == default)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            return await GetCachedDataByIdAsync(trackId, platform);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data)
 | 
			
		||||
        {
 | 
			
		||||
            query = Uri.EscapeDataString(query.Trim());
 | 
			
		||||
 | 
			
		||||
            // first cache the data
 | 
			
		||||
            await CacheTrackDataAsync(data);
 | 
			
		||||
            
 | 
			
		||||
            // then map the query to cached data's id
 | 
			
		||||
            var db = _multiplexer.GetDatabase();
 | 
			
		||||
 | 
			
		||||
            var queryDataKey = CreateCachedQueryDataKey(query, data.Platform);
 | 
			
		||||
            await db.StringSetAsync(queryDataKey, data.Id, TimeSpan.FromDays(7));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        private static string CreateCachedPlaylistKey(string playlistId, MusicPlatform platform)
 | 
			
		||||
            => $"playlist:{platform}:{playlistId}";
 | 
			
		||||
        public async Task<IReadOnlyCollection<string>> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform)
 | 
			
		||||
        {
 | 
			
		||||
            var db = _multiplexer.GetDatabase();
 | 
			
		||||
            var key = CreateCachedPlaylistKey(playlistId, platform);
 | 
			
		||||
            var vals = await db.ListRangeAsync(key);
 | 
			
		||||
            if (vals == default || vals.Length == 0)
 | 
			
		||||
                return Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
            return vals.Select(x => x.ToString()).ToList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable<string> ids)
 | 
			
		||||
        {
 | 
			
		||||
            var db = _multiplexer.GetDatabase();
 | 
			
		||||
            var key = CreateCachedPlaylistKey(playlistId, platform);
 | 
			
		||||
            await db.ListRightPushAsync(key, ids.Select(x => (RedisValue) x).ToArray());
 | 
			
		||||
            await db.KeyExpireAsync(key, TimeSpan.FromDays(7));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        private static string CreateCachedPlaylistQueryKey(string query, MusicPlatform platform)
 | 
			
		||||
            => $"playlist:query:{platform}:{query}";
 | 
			
		||||
        public Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId)
 | 
			
		||||
        {
 | 
			
		||||
            query = Uri.EscapeDataString(query.Trim());
 | 
			
		||||
            var key = CreateCachedPlaylistQueryKey(query, platform);
 | 
			
		||||
            var db = _multiplexer.GetDatabase();
 | 
			
		||||
            return db.StringSetAsync(key, playlistId, TimeSpan.FromDays(7));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<string?> GetPlaylistIdByQueryAsync(string query, MusicPlatform platform)
 | 
			
		||||
        {
 | 
			
		||||
            query = Uri.EscapeDataString(query.Trim());
 | 
			
		||||
            var key = CreateCachedPlaylistQueryKey(query, platform);
 | 
			
		||||
 | 
			
		||||
            var val = await _multiplexer.GetDatabase().StringGetAsync(key);
 | 
			
		||||
            if (val == default)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            return val;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								src/NadekoBot/Modules/Music/Common/Impl/RemoteTrackInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/NadekoBot/Modules/Music/Common/Impl/RemoteTrackInfo.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public sealed class RemoteTrackInfo : ITrackInfo
 | 
			
		||||
    {
 | 
			
		||||
        public string Title { get; }
 | 
			
		||||
        public string Url { get; }
 | 
			
		||||
        public string Thumbnail { get; }
 | 
			
		||||
        public TimeSpan Duration { get; }
 | 
			
		||||
        public MusicPlatform Platform { get; }
 | 
			
		||||
 | 
			
		||||
        private readonly Func<Task<string?>> _streamFactory;
 | 
			
		||||
 | 
			
		||||
        public RemoteTrackInfo(string title, string url, string thumbnail, TimeSpan duration, MusicPlatform platform,
 | 
			
		||||
            Func<Task<string?>> streamFactory)
 | 
			
		||||
        {
 | 
			
		||||
            _streamFactory = streamFactory;
 | 
			
		||||
            Title = title;
 | 
			
		||||
            Url = url;
 | 
			
		||||
            Thumbnail = thumbnail;
 | 
			
		||||
            Duration = duration;
 | 
			
		||||
            Platform = platform;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async ValueTask<string?> GetStreamUrl() => await _streamFactory();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/NadekoBot/Modules/Music/Common/Impl/SimpleTrackInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/NadekoBot/Modules/Music/Common/Impl/SimpleTrackInfo.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public sealed class SimpleTrackInfo : ITrackInfo
 | 
			
		||||
    {
 | 
			
		||||
        public string Title { get; }
 | 
			
		||||
        public string Url { get; }
 | 
			
		||||
        public string Thumbnail { get; }
 | 
			
		||||
        public TimeSpan Duration { get; }
 | 
			
		||||
        public MusicPlatform Platform { get; }
 | 
			
		||||
        public string? StreamUrl { get; }
 | 
			
		||||
        public ValueTask<string?> GetStreamUrl() => new ValueTask<string?>(StreamUrl);
 | 
			
		||||
 | 
			
		||||
        public SimpleTrackInfo(string title, string url, string thumbnail, TimeSpan duration,
 | 
			
		||||
            MusicPlatform platform, string streamUrl)
 | 
			
		||||
        {
 | 
			
		||||
            Title = title;
 | 
			
		||||
            Url = url;
 | 
			
		||||
            Thumbnail = thumbnail;
 | 
			
		||||
            Duration = duration;
 | 
			
		||||
            Platform = platform;
 | 
			
		||||
            StreamUrl = streamUrl;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										119
									
								
								src/NadekoBot/Modules/Music/Common/Impl/VoiceProxy.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/NadekoBot/Modules/Music/Common/Impl/VoiceProxy.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Ayu.Discord.Voice;
 | 
			
		||||
using Ayu.Discord.Voice.Models;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public sealed class VoiceProxy : IVoiceProxy
 | 
			
		||||
    {
 | 
			
		||||
        public enum VoiceProxyState
 | 
			
		||||
        {
 | 
			
		||||
            Created,
 | 
			
		||||
            Started,
 | 
			
		||||
            Stopped
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private const int MAX_ERROR_COUNT = 20;
 | 
			
		||||
        private const int DELAY_ON_ERROR_MILISECONDS = 200;
 | 
			
		||||
 | 
			
		||||
        public VoiceProxyState State
 | 
			
		||||
            => _gateway switch
 | 
			
		||||
            {
 | 
			
		||||
                {Started: true, Stopped: false} => VoiceProxyState.Started,
 | 
			
		||||
                {Stopped: false} => VoiceProxyState.Created,
 | 
			
		||||
                _ => VoiceProxyState.Stopped
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
        
 | 
			
		||||
        private VoiceGateway _gateway;
 | 
			
		||||
 | 
			
		||||
        public VoiceProxy(VoiceGateway initial)
 | 
			
		||||
        {
 | 
			
		||||
            _gateway = initial;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool SendPcmFrame(VoiceClient vc, Span<byte> data, int length)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var gw = _gateway;
 | 
			
		||||
                if (gw is null || gw.Stopped || !gw.Started)
 | 
			
		||||
                {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                vc.SendPcmFrame(gw, data, 0, length);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> RunGatewayAction(Func<VoiceGateway, Task> action)
 | 
			
		||||
        {
 | 
			
		||||
            var errorCount = 0;
 | 
			
		||||
            do
 | 
			
		||||
            {
 | 
			
		||||
                if (State == VoiceProxyState.Stopped)
 | 
			
		||||
                {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var gw = _gateway;
 | 
			
		||||
                    if (gw is null || !gw.ConnectingFinished.Task.IsCompleted)
 | 
			
		||||
                    {
 | 
			
		||||
                        ++errorCount;
 | 
			
		||||
                        await Task.Delay(DELAY_ON_ERROR_MILISECONDS);
 | 
			
		||||
                        Log.Debug("Gateway is not ready");
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await action(gw);
 | 
			
		||||
                    errorCount = 0;
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    ++errorCount;
 | 
			
		||||
                    await Task.Delay(DELAY_ON_ERROR_MILISECONDS);
 | 
			
		||||
                    Log.Debug(ex, "Error performing proxy gateway action");
 | 
			
		||||
                }
 | 
			
		||||
            } while (errorCount > 0 && errorCount <= MAX_ERROR_COUNT);
 | 
			
		||||
 | 
			
		||||
            return State != VoiceProxyState.Stopped && errorCount <= MAX_ERROR_COUNT;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void SetGateway(VoiceGateway gateway)
 | 
			
		||||
        {
 | 
			
		||||
            _gateway = gateway;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task StartSpeakingAsync()
 | 
			
		||||
        {
 | 
			
		||||
            return RunGatewayAction((gw) => gw.SendSpeakingAsync(VoiceSpeaking.State.Microphone));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task StopSpeakingAsync()
 | 
			
		||||
        {
 | 
			
		||||
            return RunGatewayAction((gw) => gw.SendSpeakingAsync(VoiceSpeaking.State.None));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task StartGateway()
 | 
			
		||||
        {
 | 
			
		||||
            await _gateway.Start();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task StopGateway()
 | 
			
		||||
        {
 | 
			
		||||
            if(_gateway is VoiceGateway gw)
 | 
			
		||||
                return gw.StopAsync();
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,133 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Modules.Music;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
#nullable enable
 | 
			
		||||
namespace NadekoBot.Modules.Music.Resolvers
 | 
			
		||||
{
 | 
			
		||||
    public sealed class LocalTrackResolver : ILocalTrackResolver
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly HashSet<string> _musicExtensions = new[]
 | 
			
		||||
        {
 | 
			
		||||
            ".MP4", ".MP3", ".FLAC", ".OGG", ".WAV", ".WMA", ".WMV",
 | 
			
		||||
            ".AAC", ".MKV", ".WEBM", ".M4A", ".AA", ".AAX",
 | 
			
		||||
            ".ALAC", ".AIFF", ".MOV", ".FLV", ".OGG", ".M4V"
 | 
			
		||||
        }.ToHashSet();
 | 
			
		||||
        
 | 
			
		||||
        public async Task<ITrackInfo?> ResolveByQueryAsync(string query)
 | 
			
		||||
        {
 | 
			
		||||
            if (!File.Exists(query))
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            var trackDuration = await Ffprobe.GetTrackDurationAsync(query);
 | 
			
		||||
            return new SimpleTrackInfo(
 | 
			
		||||
                Path.GetFileNameWithoutExtension(query),
 | 
			
		||||
                $"https://google.com?q={Uri.EscapeDataString(Path.GetFileNameWithoutExtension(query))}",
 | 
			
		||||
                "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png",
 | 
			
		||||
                trackDuration,
 | 
			
		||||
                MusicPlatform.Local,
 | 
			
		||||
                $"\"{Path.GetFullPath(query)}\""
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async IAsyncEnumerable<ITrackInfo> ResolveDirectoryAsync(string dirPath)
 | 
			
		||||
        {
 | 
			
		||||
            DirectoryInfo dir;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                dir = new DirectoryInfo(dirPath);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Specified directory {DirectoryPath} could not be opened", dirPath);
 | 
			
		||||
                yield break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var files = dir.EnumerateFiles()
 | 
			
		||||
                .Where(x =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (!x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)
 | 
			
		||||
                        && _musicExtensions.Contains(x.Extension.ToUpperInvariant())) return true;
 | 
			
		||||
                    return false;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            var firstFile = files.FirstOrDefault()?.FullName;
 | 
			
		||||
            if (firstFile is null)
 | 
			
		||||
                yield break;
 | 
			
		||||
            
 | 
			
		||||
            var firstData = await ResolveByQueryAsync(firstFile);
 | 
			
		||||
            if (!(firstData is null))
 | 
			
		||||
                yield return firstData;
 | 
			
		||||
 | 
			
		||||
            var fileChunks = files.Skip(1).Chunk(10);
 | 
			
		||||
            foreach (var chunk in fileChunks)
 | 
			
		||||
            { 
 | 
			
		||||
                var part = await Task.WhenAll(chunk.Select(x => ResolveByQueryAsync(x.FullName)));
 | 
			
		||||
                
 | 
			
		||||
                // nullable reference types being annoying
 | 
			
		||||
                foreach (var p in part)
 | 
			
		||||
                {
 | 
			
		||||
                    if (p is null)
 | 
			
		||||
                        continue;
 | 
			
		||||
 | 
			
		||||
                    yield return p;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Ffprobe
 | 
			
		||||
    {
 | 
			
		||||
        public static async Task<TimeSpan> GetTrackDurationAsync(string query)
 | 
			
		||||
        {
 | 
			
		||||
            query = query.Replace("\"", "");
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                using var p = Process.Start(new ProcessStartInfo()
 | 
			
		||||
                {
 | 
			
		||||
                    FileName = "ffprobe",
 | 
			
		||||
                    Arguments =
 | 
			
		||||
                        $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -- \"{query}\"",
 | 
			
		||||
                    UseShellExecute = false,
 | 
			
		||||
                    RedirectStandardOutput = true,
 | 
			
		||||
                    RedirectStandardError = true,
 | 
			
		||||
                    StandardOutputEncoding = Encoding.UTF8,
 | 
			
		||||
                    StandardErrorEncoding = Encoding.UTF8,
 | 
			
		||||
                    CreateNoWindow = true,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                if (p is null)
 | 
			
		||||
                    return TimeSpan.Zero;
 | 
			
		||||
 | 
			
		||||
                var data = await p.StandardOutput.ReadToEndAsync();
 | 
			
		||||
                if (double.TryParse(data, out var seconds))
 | 
			
		||||
                    return TimeSpan.FromSeconds(seconds);
 | 
			
		||||
 | 
			
		||||
                var errorData = await p.StandardError.ReadToEndAsync();
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(errorData))
 | 
			
		||||
                    Log.Warning("Ffprobe warning for file {FileName}: {ErrorMessage}", query, errorData);
 | 
			
		||||
 | 
			
		||||
                return TimeSpan.Zero;
 | 
			
		||||
            }
 | 
			
		||||
            catch (Win32Exception)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Ffprobe was likely not installed. Local song durations will show as (?)");
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Unknown exception running ffprobe; {ErrorMessage}", ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            return TimeSpan.Zero;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,135 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Modules.Music;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Resolvers
 | 
			
		||||
{
 | 
			
		||||
    public class RadioResolver : IRadioResolver
 | 
			
		||||
    {
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
        public RadioResolver()
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<ITrackInfo> ResolveByQueryAsync(string query)
 | 
			
		||||
        {
 | 
			
		||||
            if (IsRadioLink(query))
 | 
			
		||||
                query = await HandleStreamContainers(query).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            return new SimpleTrackInfo(
 | 
			
		||||
                query.TrimTo(50),
 | 
			
		||||
                query,
 | 
			
		||||
                "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png",
 | 
			
		||||
                TimeSpan.MaxValue,
 | 
			
		||||
                MusicPlatform.Radio,
 | 
			
		||||
                query
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static bool IsRadioLink(string query) =>
 | 
			
		||||
            (query.StartsWith("http", StringComparison.InvariantCulture) ||
 | 
			
		||||
            query.StartsWith("ww", StringComparison.InvariantCulture))
 | 
			
		||||
            &&
 | 
			
		||||
            (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.Warning($"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.Warning($"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.Warning($"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.Warning($"Failed reading .xspf:\n{file}");
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return query;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,101 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using NadekoBot.Core.Services.Impl;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Modules.Music;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
#nullable enable
 | 
			
		||||
namespace NadekoBot.Modules.Music.Resolvers
 | 
			
		||||
{   
 | 
			
		||||
    public sealed class SoundcloudResolver : ISoundcloudResolver
 | 
			
		||||
    {
 | 
			
		||||
        private readonly SoundCloudApiService _sc;
 | 
			
		||||
        private readonly ITrackCacher _trackCacher;
 | 
			
		||||
        private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
 | 
			
		||||
        public SoundcloudResolver(SoundCloudApiService sc, ITrackCacher trackCacher, IHttpClientFactory httpFactory)
 | 
			
		||||
        {
 | 
			
		||||
            _sc = sc;
 | 
			
		||||
            _trackCacher = trackCacher;
 | 
			
		||||
            _httpFactory = httpFactory;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool IsSoundCloudLink(string url) =>
 | 
			
		||||
            System.Text.RegularExpressions.Regex.IsMatch(url, "(.*)(soundcloud.com|snd.sc)(.*)");
 | 
			
		||||
 | 
			
		||||
        public async IAsyncEnumerable<ITrackInfo> ResolvePlaylistAsync(string playlist)
 | 
			
		||||
        {
 | 
			
		||||
            playlist = Uri.EscapeDataString(playlist);
 | 
			
		||||
            
 | 
			
		||||
            using var http = _httpFactory.CreateClient();
 | 
			
		||||
            var responseString = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={playlist}");
 | 
			
		||||
            var scvids = JObject.Parse(responseString)["tracks"]?.ToObject<SoundCloudVideo[]>();
 | 
			
		||||
            if (scvids is null)
 | 
			
		||||
            {
 | 
			
		||||
                yield break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var videosChunk in scvids.Where(x => x.Streamable is true).Chunk(5))
 | 
			
		||||
            {
 | 
			
		||||
                var cachableTracks = videosChunk
 | 
			
		||||
                    .Select(VideoModelToCachedData)
 | 
			
		||||
                    .ToList();
 | 
			
		||||
 | 
			
		||||
                await Task.WhenAll(cachableTracks.Select(_trackCacher.CacheTrackDataAsync));
 | 
			
		||||
                foreach(var info in cachableTracks.Select(CachableDataToTrackInfo))
 | 
			
		||||
                {
 | 
			
		||||
                    yield return info;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private ICachableTrackData VideoModelToCachedData(SoundCloudVideo svideo)
 | 
			
		||||
            => new CachableTrackData()
 | 
			
		||||
            {
 | 
			
		||||
                Title = svideo.FullName,
 | 
			
		||||
                Url = svideo.TrackLink,
 | 
			
		||||
                Thumbnail = svideo.ArtworkUrl,
 | 
			
		||||
                TotalDurationMs = svideo.Duration,
 | 
			
		||||
                Id = svideo.Id.ToString(),
 | 
			
		||||
                Platform = MusicPlatform.SoundCloud
 | 
			
		||||
            };
 | 
			
		||||
        
 | 
			
		||||
        private ITrackInfo CachableDataToTrackInfo(ICachableTrackData trackData)
 | 
			
		||||
            => new SimpleTrackInfo(
 | 
			
		||||
                trackData.Title,
 | 
			
		||||
                trackData.Url,
 | 
			
		||||
                trackData.Thumbnail,
 | 
			
		||||
                trackData.Duration,
 | 
			
		||||
                trackData.Platform,
 | 
			
		||||
                GetStreamUrl(trackData.Id)
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        private string GetStreamUrl(string trackId)
 | 
			
		||||
            => $"https://api.soundcloud.com/tracks/{trackId}/stream?client_id=368b0c85751007cd588d869d3ae61ac0";
 | 
			
		||||
 | 
			
		||||
        public async Task<ITrackInfo?> ResolveByQueryAsync(string query)
 | 
			
		||||
        {
 | 
			
		||||
            var cached = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.SoundCloud);
 | 
			
		||||
            if (!(cached is null))
 | 
			
		||||
                return CachableDataToTrackInfo(cached);
 | 
			
		||||
            
 | 
			
		||||
            var svideo = !IsSoundCloudLink(query)
 | 
			
		||||
                ? await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false)
 | 
			
		||||
                : await _sc.ResolveVideoAsync(query).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            if (svideo is null)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            var cachableData = VideoModelToCachedData(svideo);
 | 
			
		||||
            await _trackCacher.CacheTrackDataByQueryAsync(query, cachableData);
 | 
			
		||||
            
 | 
			
		||||
            return CachableDataToTrackInfo(cachableData);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public sealed class TrackResolveProvider : ITrackResolveProvider
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IYoutubeResolver _ytResolver;
 | 
			
		||||
        private readonly ILocalTrackResolver _localResolver;
 | 
			
		||||
        private readonly ISoundcloudResolver _soundcloudResolver;
 | 
			
		||||
        private readonly IRadioResolver _radioResolver;
 | 
			
		||||
 | 
			
		||||
        public TrackResolveProvider(IYoutubeResolver ytResolver, ILocalTrackResolver localResolver,
 | 
			
		||||
            ISoundcloudResolver soundcloudResolver, IRadioResolver radioResolver)
 | 
			
		||||
        {
 | 
			
		||||
            _ytResolver = ytResolver;
 | 
			
		||||
            _localResolver = localResolver;
 | 
			
		||||
            _soundcloudResolver = soundcloudResolver;
 | 
			
		||||
            _radioResolver = radioResolver;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<ITrackInfo?> QuerySongAsync(string query, MusicPlatform? forcePlatform)
 | 
			
		||||
        {
 | 
			
		||||
            switch (forcePlatform)
 | 
			
		||||
            {
 | 
			
		||||
                case MusicPlatform.Radio:
 | 
			
		||||
                    return _radioResolver.ResolveByQueryAsync(query);
 | 
			
		||||
                case MusicPlatform.Youtube:
 | 
			
		||||
                    return _ytResolver.ResolveByQueryAsync(query);
 | 
			
		||||
                case MusicPlatform.Local:
 | 
			
		||||
                    return _localResolver.ResolveByQueryAsync(query);
 | 
			
		||||
                case MusicPlatform.SoundCloud:
 | 
			
		||||
                    return _soundcloudResolver.ResolveByQueryAsync(query);
 | 
			
		||||
                case null:
 | 
			
		||||
                    var match = _ytResolver.YtVideoIdRegex.Match(query);
 | 
			
		||||
                    if (match.Success)
 | 
			
		||||
                        return _ytResolver.ResolveByIdAsync(match.Groups["id"].Value);
 | 
			
		||||
                    else if (_soundcloudResolver.IsSoundCloudLink(query))
 | 
			
		||||
                        return _soundcloudResolver.ResolveByQueryAsync(query);
 | 
			
		||||
                    else if (Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.IsFile)
 | 
			
		||||
                        return _localResolver.ResolveByQueryAsync(uri.AbsolutePath);
 | 
			
		||||
                    else if (IsRadioLink(query))
 | 
			
		||||
                        return _radioResolver.ResolveByQueryAsync(query);
 | 
			
		||||
                    else
 | 
			
		||||
                        return _ytResolver.ResolveByQueryAsync(query, false);
 | 
			
		||||
                default:
 | 
			
		||||
                    Log.Error("Unsupported platform: {MusicPlatform}", forcePlatform);
 | 
			
		||||
                    return Task.FromResult<ITrackInfo?>(null);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public static bool IsRadioLink(string query) =>
 | 
			
		||||
            (query.StartsWith("http", StringComparison.InvariantCulture) ||
 | 
			
		||||
             query.StartsWith("ww", StringComparison.InvariantCulture))
 | 
			
		||||
            &&
 | 
			
		||||
            (query.Contains(".pls") ||
 | 
			
		||||
             query.Contains(".m3u") ||
 | 
			
		||||
             query.Contains(".asx") ||
 | 
			
		||||
             query.Contains(".xspf"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,354 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Impl;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public sealed class YtdlYoutubeResolver : IYoutubeResolver
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly string[] durationFormats = new[]
 | 
			
		||||
            {"ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss"};
 | 
			
		||||
 | 
			
		||||
        public Regex YtVideoIdRegex { get; }
 | 
			
		||||
            = new Regex(
 | 
			
		||||
                @"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{6,11})",
 | 
			
		||||
                RegexOptions.Compiled
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        private readonly ITrackCacher _trackCacher;
 | 
			
		||||
        
 | 
			
		||||
        private readonly YtdlOperation _ytdlPlaylistOperation;
 | 
			
		||||
        private readonly YtdlOperation _ytdlIdOperation;
 | 
			
		||||
        private readonly YtdlOperation _ytdlSearchOperation;
 | 
			
		||||
 | 
			
		||||
        private IGoogleApiService _google;
 | 
			
		||||
 | 
			
		||||
        public YtdlYoutubeResolver(ITrackCacher trackCacher, IGoogleApiService google)
 | 
			
		||||
        {
 | 
			
		||||
            _trackCacher = trackCacher;
 | 
			
		||||
            _google = google;
 | 
			
		||||
 | 
			
		||||
            _ytdlPlaylistOperation = 
 | 
			
		||||
                new YtdlOperation("-4 " +
 | 
			
		||||
                                  "--geo-bypass " +
 | 
			
		||||
                                  "--encoding UTF8 " +
 | 
			
		||||
                                  "-f bestaudio " +
 | 
			
		||||
                                  "-e " +
 | 
			
		||||
                                  "--get-url " +
 | 
			
		||||
                                  "--get-id " +
 | 
			
		||||
                                  "--get-thumbnail " +
 | 
			
		||||
                                  "--get-duration " +
 | 
			
		||||
                                  "--no-check-certificate " +
 | 
			
		||||
                                  "-i " +
 | 
			
		||||
                                  "--yes-playlist " +
 | 
			
		||||
                                  "-- \"{0}\"");
 | 
			
		||||
 | 
			
		||||
            _ytdlIdOperation = 
 | 
			
		||||
                new YtdlOperation("-4 " +
 | 
			
		||||
                                  "--geo-bypass " +
 | 
			
		||||
                                  "--encoding UTF8 " +
 | 
			
		||||
                                  "-f bestaudio " +
 | 
			
		||||
                                  "-e " +
 | 
			
		||||
                                  "--get-url " +
 | 
			
		||||
                                  "--get-id " +
 | 
			
		||||
                                  "--get-thumbnail " +
 | 
			
		||||
                                  "--get-duration " +
 | 
			
		||||
                                  "--no-check-certificate " +
 | 
			
		||||
                                  "-- \"{0}\"");
 | 
			
		||||
            
 | 
			
		||||
            _ytdlSearchOperation = 
 | 
			
		||||
                new YtdlOperation("-4 " +
 | 
			
		||||
                                  "--geo-bypass " +
 | 
			
		||||
                                  "--encoding UTF8 " +
 | 
			
		||||
                                  "-f bestaudio " +
 | 
			
		||||
                                  "-e " +
 | 
			
		||||
                                  "--get-url " +
 | 
			
		||||
                                  "--get-id " +
 | 
			
		||||
                                  "--get-thumbnail " +
 | 
			
		||||
                                  "--get-duration " +
 | 
			
		||||
                                  "--no-check-certificate " +
 | 
			
		||||
                                  "--default-search " +
 | 
			
		||||
                                  "\"ytsearch:\" -- \"{0}\"");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly struct YtTrackData
 | 
			
		||||
        {
 | 
			
		||||
            public readonly string Title;
 | 
			
		||||
            public readonly string Id;
 | 
			
		||||
            public readonly string Thumbnail;
 | 
			
		||||
            public readonly string? StreamUrl;
 | 
			
		||||
            public readonly TimeSpan Duration;
 | 
			
		||||
 | 
			
		||||
            public YtTrackData(string title, string id, string thumbnail, string? streamUrl, TimeSpan duration)
 | 
			
		||||
            {
 | 
			
		||||
                Title = title.Trim();
 | 
			
		||||
                Id = id.Trim();
 | 
			
		||||
                Thumbnail = thumbnail;
 | 
			
		||||
                StreamUrl = streamUrl;
 | 
			
		||||
                Duration = duration;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private YtTrackData ResolveYtdlData(string ytdlOutputString)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(ytdlOutputString))
 | 
			
		||||
                return default;
 | 
			
		||||
            
 | 
			
		||||
            var dataArray = ytdlOutputString.Trim().Split('\n');
 | 
			
		||||
 | 
			
		||||
            if (dataArray.Length < 5)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("Not enough data received: {YtdlData}", ytdlOutputString);
 | 
			
		||||
                return default;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!TimeSpan.TryParseExact(dataArray[4], durationFormats, CultureInfo.InvariantCulture, out var time))
 | 
			
		||||
            {
 | 
			
		||||
                time = TimeSpan.Zero;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var thumbnail = Uri.IsWellFormedUriString(dataArray[3], UriKind.Absolute)
 | 
			
		||||
                ? dataArray[3].Trim()
 | 
			
		||||
                : string.Empty;
 | 
			
		||||
 | 
			
		||||
            return new YtTrackData(
 | 
			
		||||
                dataArray[0],
 | 
			
		||||
                dataArray[1],
 | 
			
		||||
                thumbnail,
 | 
			
		||||
                dataArray[2],
 | 
			
		||||
                time
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private ITrackInfo DataToInfo(in YtTrackData trackData)
 | 
			
		||||
            => new RemoteTrackInfo(
 | 
			
		||||
                trackData.Title,
 | 
			
		||||
                $"https://youtube.com/watch?v={trackData.Id}",
 | 
			
		||||
                trackData.Thumbnail,
 | 
			
		||||
                trackData.Duration,
 | 
			
		||||
                MusicPlatform.Youtube,
 | 
			
		||||
                CreateCacherFactory(trackData.Id));
 | 
			
		||||
 | 
			
		||||
        private Func<Task<string?>> CreateCacherFactory(string id)
 | 
			
		||||
            => () => _trackCacher.GetOrCreateStreamLink(
 | 
			
		||||
                id,
 | 
			
		||||
                MusicPlatform.Youtube,
 | 
			
		||||
                async () => await ExtractNewStreamUrlAsync(id)
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        private static readonly Regex expiryRegex = new Regex(@"(?:[\?\&]expire\=(?<timestamp>\d+))");
 | 
			
		||||
        private static TimeSpan GetExpiry(string streamUrl)
 | 
			
		||||
        {
 | 
			
		||||
            var match = expiryRegex.Match(streamUrl);
 | 
			
		||||
            if (match.Success && double.TryParse(match.Groups["timestamp"].ToString(), out var timestamp))
 | 
			
		||||
            {
 | 
			
		||||
                var realExpiry = (timestamp.ToUnixTimestamp() - DateTime.UtcNow);
 | 
			
		||||
                if (realExpiry > TimeSpan.FromMinutes(60))
 | 
			
		||||
                    return realExpiry.Subtract(TimeSpan.FromMinutes(30));
 | 
			
		||||
 | 
			
		||||
                return realExpiry;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return TimeSpan.FromHours(1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<(string StreamUrl, TimeSpan Expiry)> ExtractNewStreamUrlAsync(string id)
 | 
			
		||||
        {
 | 
			
		||||
            var data = await _ytdlIdOperation.GetDataAsync(id);
 | 
			
		||||
            var trackInfo = ResolveYtdlData(data);
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(trackInfo.StreamUrl))
 | 
			
		||||
                return default;
 | 
			
		||||
            
 | 
			
		||||
            return (trackInfo.StreamUrl!, GetExpiry(trackInfo.StreamUrl!));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<ITrackInfo?> ResolveByIdAsync(string id)
 | 
			
		||||
        {
 | 
			
		||||
            id = id.Trim();
 | 
			
		||||
            
 | 
			
		||||
            var cachedData = await _trackCacher.GetCachedDataByIdAsync(id, MusicPlatform.Youtube);
 | 
			
		||||
            if (cachedData is null)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("Resolving youtube track by Id: {YoutubeId}", id);
 | 
			
		||||
 | 
			
		||||
                var data = await _ytdlIdOperation.GetDataAsync(id);
 | 
			
		||||
                
 | 
			
		||||
                var trackInfo = ResolveYtdlData(data);
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(trackInfo.Title))
 | 
			
		||||
                    return default;
 | 
			
		||||
                
 | 
			
		||||
                var toReturn = DataToInfo(in trackInfo);
 | 
			
		||||
 | 
			
		||||
                await Task.WhenAll(
 | 
			
		||||
                    _trackCacher.CacheTrackDataAsync(toReturn.ToCachedData(id)),
 | 
			
		||||
                    CacheStreamUrlAsync(trackInfo)
 | 
			
		||||
                );
 | 
			
		||||
                
 | 
			
		||||
                return toReturn;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return DataToInfo(new YtTrackData(
 | 
			
		||||
                cachedData.Title,
 | 
			
		||||
                cachedData.Id,
 | 
			
		||||
                cachedData.Thumbnail,
 | 
			
		||||
                null,
 | 
			
		||||
                cachedData.Duration
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task CacheStreamUrlAsync(YtTrackData trackInfo)
 | 
			
		||||
            => _trackCacher.CacheStreamUrlAsync(
 | 
			
		||||
                trackInfo.Id,
 | 
			
		||||
                MusicPlatform.Youtube,
 | 
			
		||||
                trackInfo.StreamUrl!,
 | 
			
		||||
                GetExpiry(trackInfo.StreamUrl!)
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        private static readonly Regex _simplePlaylistRegex
 | 
			
		||||
            = new Regex(@"&list=(?<id>[\w\-]{12,})", RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
        public async IAsyncEnumerable<ITrackInfo> ResolveTracksByPlaylistIdAsync(string playlistId)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("Resolving youtube tracks from playlist: {PlaylistId}", playlistId);
 | 
			
		||||
            var count = 0;
 | 
			
		||||
 | 
			
		||||
            var ids = await _trackCacher.GetPlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube);
 | 
			
		||||
            if (ids.Count > 0)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var id in ids)
 | 
			
		||||
                {
 | 
			
		||||
                    var trackInfo = await ResolveByIdAsync(id);
 | 
			
		||||
                    if (trackInfo is null)
 | 
			
		||||
                        continue;
 | 
			
		||||
 | 
			
		||||
                    yield return trackInfo;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                yield break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var data = string.Empty;
 | 
			
		||||
            var trackIds = new List<string>();
 | 
			
		||||
            await foreach (var line in _ytdlPlaylistOperation.EnumerateDataAsync(playlistId))
 | 
			
		||||
            {
 | 
			
		||||
                data += line;
 | 
			
		||||
 | 
			
		||||
                if (++count == 5)
 | 
			
		||||
                {
 | 
			
		||||
                    var trackData = ResolveYtdlData(data);
 | 
			
		||||
                    data = string.Empty;
 | 
			
		||||
                    count = 0;
 | 
			
		||||
                    if (string.IsNullOrWhiteSpace(trackData.Id))
 | 
			
		||||
                        continue;
 | 
			
		||||
 | 
			
		||||
                    var info = DataToInfo(in trackData);
 | 
			
		||||
                    await Task.WhenAll(
 | 
			
		||||
                        _trackCacher.CacheTrackDataAsync(info.ToCachedData(trackData.Id)),
 | 
			
		||||
                        CacheStreamUrlAsync(trackData)
 | 
			
		||||
                    );
 | 
			
		||||
                    
 | 
			
		||||
                    trackIds.Add(trackData.Id);
 | 
			
		||||
                    yield return info;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    data += Environment.NewLine;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _trackCacher.CachePlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube, trackIds);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async IAsyncEnumerable<ITrackInfo> ResolveTracksFromPlaylistAsync(string query)
 | 
			
		||||
        {
 | 
			
		||||
            string? playlistId;
 | 
			
		||||
            // try to match playlist id inside the query, if a playlist url has been queried
 | 
			
		||||
            var match = _simplePlaylistRegex.Match(query);
 | 
			
		||||
            if (match.Success)
 | 
			
		||||
            {
 | 
			
		||||
                // if it's a success, just return from that playlist using the id
 | 
			
		||||
                playlistId = match.Groups["id"].ToString();
 | 
			
		||||
                await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId))
 | 
			
		||||
                    yield return track;
 | 
			
		||||
 | 
			
		||||
                yield break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // if a query is a search term, try the cache
 | 
			
		||||
            playlistId = await _trackCacher.GetPlaylistIdByQueryAsync(query, MusicPlatform.Youtube);
 | 
			
		||||
            if (playlistId is null)
 | 
			
		||||
            {
 | 
			
		||||
                // if it's not in the cache
 | 
			
		||||
                // find playlist id by keyword using google api
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var playlistIds = await _google.GetPlaylistIdsByKeywordsAsync(query);
 | 
			
		||||
                    playlistId = playlistIds.FirstOrDefault();
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "Error Getting playlist id via GoogleApi");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // if query is not a playlist url
 | 
			
		||||
                // and query result is not in the cache
 | 
			
		||||
                // and api returns no values
 | 
			
		||||
                // it means invalid input has been used,
 | 
			
		||||
                // or google api key is not provided
 | 
			
		||||
                if (playlistId is null)
 | 
			
		||||
                    yield break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // cache the query -> playlist id for fast future lookup
 | 
			
		||||
            await _trackCacher.CachePlaylistIdByQueryAsync(query, MusicPlatform.Youtube, playlistId);
 | 
			
		||||
            await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId))
 | 
			
		||||
                yield return track;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<ITrackInfo?> ResolveByQueryAsync(string query)
 | 
			
		||||
            => ResolveByQueryAsync(query, true);
 | 
			
		||||
        
 | 
			
		||||
        public async Task<ITrackInfo?> ResolveByQueryAsync(string query, bool tryResolving)
 | 
			
		||||
        {
 | 
			
		||||
            if (tryResolving)
 | 
			
		||||
            {
 | 
			
		||||
                var match = YtVideoIdRegex.Match(query);
 | 
			
		||||
                if (match.Success)
 | 
			
		||||
                    return await ResolveByIdAsync(match.Groups["id"].Value);
 | 
			
		||||
            }
 | 
			
		||||
                
 | 
			
		||||
            Log.Information("Resolving youtube song by search term: {YoutubeQuery}", query);
 | 
			
		||||
            
 | 
			
		||||
            var cachedData = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.Youtube);
 | 
			
		||||
            if (cachedData is null)
 | 
			
		||||
            {
 | 
			
		||||
                var stringData = await _ytdlSearchOperation.GetDataAsync(query);
 | 
			
		||||
                var trackData = ResolveYtdlData(stringData);
 | 
			
		||||
                
 | 
			
		||||
                var trackInfo = DataToInfo(trackData);
 | 
			
		||||
                await Task.WhenAll(
 | 
			
		||||
                    _trackCacher.CacheTrackDataByQueryAsync(query, trackInfo.ToCachedData(trackData.Id)),
 | 
			
		||||
                    CacheStreamUrlAsync(trackData)
 | 
			
		||||
                );
 | 
			
		||||
                return trackInfo;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            return DataToInfo(new YtTrackData(
 | 
			
		||||
                cachedData.Title,
 | 
			
		||||
                cachedData.Id,
 | 
			
		||||
                cachedData.Thumbnail,
 | 
			
		||||
                null,
 | 
			
		||||
                cachedData.Duration
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										717
									
								
								src/NadekoBot/Modules/Music/Common/_OldMusicPlayer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										717
									
								
								src/NadekoBot/Modules/Music/Common/_OldMusicPlayer.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,717 @@
 | 
			
		||||
// using Discord;
 | 
			
		||||
// using Discord.Audio;
 | 
			
		||||
// using System;
 | 
			
		||||
// using System.Threading;
 | 
			
		||||
// using System.Threading.Tasks;
 | 
			
		||||
// using System.Linq;
 | 
			
		||||
// using System.Runtime.InteropServices;
 | 
			
		||||
// using NadekoBot.Extensions;
 | 
			
		||||
// using NadekoBot.Common.Collections;
 | 
			
		||||
// using NadekoBot.Modules.Music.Services;
 | 
			
		||||
// using NadekoBot.Core.Services;
 | 
			
		||||
// using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
// using Discord.WebSocket;
 | 
			
		||||
// using Serilog;
 | 
			
		||||
//
 | 
			
		||||
// 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; }
 | 
			
		||||
//
 | 
			
		||||
//         public ITextChannel OriginalTextChannel { get; set; }
 | 
			
		||||
//         
 | 
			
		||||
//         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 (Index, Song) = Queue.Current;
 | 
			
		||||
//                     if (Song != null)
 | 
			
		||||
//                         RecentlyPlayedUsers.Add(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, MusicSettings ms, IGoogleApiService google,
 | 
			
		||||
//             IVoiceChannel vch, ITextChannel original, float volume)
 | 
			
		||||
//         {
 | 
			
		||||
//             this.Volume = volume;
 | 
			
		||||
//             this.VoiceChannel = vch;
 | 
			
		||||
//             this.OriginalTextChannel = original;
 | 
			
		||||
//             this.SongCancelSource = new CancellationTokenSource();
 | 
			
		||||
//             if (ms.MusicChannelId is ulong cid)
 | 
			
		||||
//             {
 | 
			
		||||
//                 this.OutputTextChannel = ((SocketGuild)original.Guild).GetTextChannel(cid) ?? original;
 | 
			
		||||
//             }
 | 
			
		||||
//             else
 | 
			
		||||
//             {
 | 
			
		||||
//                 this.OutputTextChannel = original;
 | 
			
		||||
//             }
 | 
			
		||||
//             this._musicService = musicService;
 | 
			
		||||
//             this.AutoDelete = ms.SongAutoDelete;
 | 
			
		||||
//             this._google = google;
 | 
			
		||||
//
 | 
			
		||||
//             _player = new Thread(new ThreadStart(PlayerLoop))
 | 
			
		||||
//             {
 | 
			
		||||
//                 Priority = ThreadPriority.AboveNormal
 | 
			
		||||
//             };
 | 
			
		||||
//             _player.Start();
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         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.Information("Starting");
 | 
			
		||||
//                     AudioOutStream pcm = null;
 | 
			
		||||
//                     SongBuffer b = null;
 | 
			
		||||
//                     try
 | 
			
		||||
//                     {
 | 
			
		||||
//                         var streamUrl = await data.Song.Uri().ConfigureAwait(false);
 | 
			
		||||
//                         b = new SongBuffer(streamUrl, data.Song.ProviderType == MusicType.Local);
 | 
			
		||||
//                         //Log.Information("Created buffer, buffering...");
 | 
			
		||||
//
 | 
			
		||||
//                         //var bufferTask = b.StartBuffering(cancelToken);
 | 
			
		||||
//                         //var timeout = Task.Delay(10000);
 | 
			
		||||
//                         //if (Task.WhenAny(bufferTask, timeout) == timeout)
 | 
			
		||||
//                         //{
 | 
			
		||||
//                         //    Log.Information("Buffering failed due to a timeout.");
 | 
			
		||||
//                         //    continue;
 | 
			
		||||
//                         //}
 | 
			
		||||
//                         //else if (!bufferTask.Result)
 | 
			
		||||
//                         //{
 | 
			
		||||
//                         //    Log.Information("Buffering failed due to a cancel or error.");
 | 
			
		||||
//                         //    continue;
 | 
			
		||||
//                         //}
 | 
			
		||||
//                         //Log.Information("Buffered. Getting audio client...");
 | 
			
		||||
//                         var ac = await GetAudioClient().ConfigureAwait(false);
 | 
			
		||||
//                         Log.Information("Got Audio client");
 | 
			
		||||
//                         if (ac == null)
 | 
			
		||||
//                         {
 | 
			
		||||
//                             Log.Information("Can't join");
 | 
			
		||||
//                             await Task.Delay(900, cancelToken).ConfigureAwait(false);
 | 
			
		||||
//                             // 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;
 | 
			
		||||
//                         }
 | 
			
		||||
//                         b.StartBuffering();
 | 
			
		||||
//                         await Task.WhenAny(Task.Delay(10000), b.PrebufferingCompleted.Task).ConfigureAwait(false);
 | 
			
		||||
//                         pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 1, packetLoss: 5);
 | 
			
		||||
//                         Log.Information("Created pcm stream");
 | 
			
		||||
//                         OnStarted?.Invoke(this, data);
 | 
			
		||||
//
 | 
			
		||||
//                         while (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)
 | 
			
		||||
//                         {
 | 
			
		||||
//                             var buffer = b.Read(3840);
 | 
			
		||||
//                             if (buffer.Length == 0)
 | 
			
		||||
//                                 break;
 | 
			
		||||
//                             AdjustVolume(buffer, Volume);
 | 
			
		||||
//                             await pcm.WriteAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false);
 | 
			
		||||
//                             unchecked { _bytesSent += buffer.Length; }
 | 
			
		||||
//
 | 
			
		||||
//                             await (PauseTaskSource?.Task ?? Task.CompletedTask).ConfigureAwait(false);
 | 
			
		||||
//                         }
 | 
			
		||||
//                     }
 | 
			
		||||
//                     catch (OperationCanceledException)
 | 
			
		||||
//                     {
 | 
			
		||||
//                         Log.Information("Song Canceled");
 | 
			
		||||
//                         cancel = true;
 | 
			
		||||
//                     }
 | 
			
		||||
//                     catch (Exception ex)
 | 
			
		||||
//                     {
 | 
			
		||||
//                         Log.Warning(ex, "Error sending song data");
 | 
			
		||||
//                     }
 | 
			
		||||
//                     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)).ConfigureAwait(false);
 | 
			
		||||
//                             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.Information("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.Information("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.Information("Loading related song");
 | 
			
		||||
//                                         await _musicService.TryQueueRelatedSongAsync(data.Song, OutputTextChannel, VoiceChannel).ConfigureAwait(false);
 | 
			
		||||
//                                         if (!AutoDelete)
 | 
			
		||||
//                                             Queue.Next();
 | 
			
		||||
//                                     }
 | 
			
		||||
//                                     catch
 | 
			
		||||
//                                     {
 | 
			
		||||
//                                         Log.Information("Loading related song failed");
 | 
			
		||||
//                                     }
 | 
			
		||||
//                                 }
 | 
			
		||||
//                                 else if (FairPlay)
 | 
			
		||||
//                                 {
 | 
			
		||||
//                                     lock (locker)
 | 
			
		||||
//                                     {
 | 
			
		||||
//                                         Log.Information("Next fair song");
 | 
			
		||||
//                                         var queueList = Queue.ToList();
 | 
			
		||||
//                                         var q = queueList.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 = queueList.IndexOf(q[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.Information("Stopping because repeatplaylist is disabled");
 | 
			
		||||
//                                     lock (locker)
 | 
			
		||||
//                                     {
 | 
			
		||||
//                                         Stop();
 | 
			
		||||
//                                     }
 | 
			
		||||
//                                 }
 | 
			
		||||
//                                 else
 | 
			
		||||
//                                 {
 | 
			
		||||
//                                     Log.Information("Next song");
 | 
			
		||||
//                                     lock (locker)
 | 
			
		||||
//                                     {
 | 
			
		||||
//                                         if (!Stopped)
 | 
			
		||||
//                                             if (!AutoDelete)
 | 
			
		||||
//                                                 Queue.Next();
 | 
			
		||||
//                                     }
 | 
			
		||||
//                                 }
 | 
			
		||||
//                             }
 | 
			
		||||
//                         }
 | 
			
		||||
//                     }
 | 
			
		||||
//                     catch (Exception ex)
 | 
			
		||||
//                     {
 | 
			
		||||
//                         Log.Error(ex, "Error in queue");
 | 
			
		||||
//                     }
 | 
			
		||||
//                 }
 | 
			
		||||
//                 do
 | 
			
		||||
//                 {
 | 
			
		||||
//                     await Task.Delay(500).ConfigureAwait(false);
 | 
			
		||||
//                 }
 | 
			
		||||
//                 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.Information("Stopping audio client");
 | 
			
		||||
//                             await t.ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//                             Log.Information("Disposing audio client");
 | 
			
		||||
//                             _audioClient.Dispose();
 | 
			
		||||
//                         }
 | 
			
		||||
//                     }
 | 
			
		||||
//                     catch
 | 
			
		||||
//                     {
 | 
			
		||||
//                     }
 | 
			
		||||
//                     newVoiceChannel = false;
 | 
			
		||||
//
 | 
			
		||||
//                     var curUser = await VoiceChannel.Guild.GetCurrentUserAsync().ConfigureAwait(false);
 | 
			
		||||
//                     if (curUser.VoiceChannel != null)
 | 
			
		||||
//                     {
 | 
			
		||||
//                         Log.Information("Connecting");
 | 
			
		||||
//                         var ac = await VoiceChannel.ConnectAsync().ConfigureAwait(false);
 | 
			
		||||
//                         Log.Information("Connected, stopping");
 | 
			
		||||
//                         await ac.StopAsync().ConfigureAwait(false);
 | 
			
		||||
//                         Log.Information("Disconnected");
 | 
			
		||||
//                         await Task.Delay(1000).ConfigureAwait(false);
 | 
			
		||||
//                     }
 | 
			
		||||
//                     Log.Information("Connecting");
 | 
			
		||||
//                     _audioClient = await VoiceChannel.ConnectAsync().ConfigureAwait(false);
 | 
			
		||||
//                 }
 | 
			
		||||
//                 catch (Exception ex)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     Log.Warning("Error while getting audio client: {0}", ex.ToString());
 | 
			
		||||
//                     return null;
 | 
			
		||||
//                 }
 | 
			
		||||
//             return _audioClient;
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public int Enqueue(SongInfo song, bool forcePlay = false)
 | 
			
		||||
//         {
 | 
			
		||||
//             lock (locker)
 | 
			
		||||
//             {
 | 
			
		||||
//                 if (Exited)
 | 
			
		||||
//                     return -1;
 | 
			
		||||
//                 Queue.Add(song);
 | 
			
		||||
//                 var result = Queue.Count - 1;
 | 
			
		||||
//
 | 
			
		||||
//                 if (forcePlay)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     if (Stopped)
 | 
			
		||||
//                     {
 | 
			
		||||
//                         Stopped = false;
 | 
			
		||||
//                         SetIndex(result);
 | 
			
		||||
//                     }
 | 
			
		||||
//                     Unpause();
 | 
			
		||||
//                 }
 | 
			
		||||
//                 return result;
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public int EnqueueNext(SongInfo song, bool forcePlay = false)
 | 
			
		||||
//         {
 | 
			
		||||
//             lock (locker)
 | 
			
		||||
//             {
 | 
			
		||||
//                 if (Exited)
 | 
			
		||||
//                     return -1;
 | 
			
		||||
//                 var toReturn = Queue.AddNext(song);
 | 
			
		||||
//                 if (forcePlay)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     Unpause();
 | 
			
		||||
//                     if (Stopped)
 | 
			
		||||
//                     {
 | 
			
		||||
//                         SetIndex(toReturn);
 | 
			
		||||
//                     }
 | 
			
		||||
//                 }
 | 
			
		||||
//                 return toReturn;
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         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() && !Autoplay) // 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;
 | 
			
		||||
//                 Autoplay = false;
 | 
			
		||||
//                 //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 (Index, Song) = Queue.Current;
 | 
			
		||||
//                 var toReturn = Queue.RemoveAt(index);
 | 
			
		||||
//                 if (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;
 | 
			
		||||
//         // }
 | 
			
		||||
//         
 | 
			
		||||
//         private static void AdjustVolume(byte[] audioSamples, float volume)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (Math.Abs(volume - 1f) < 0.0001f) return;
 | 
			
		||||
//             
 | 
			
		||||
//             var samples = MemoryMarshal.Cast<byte, short>(audioSamples);
 | 
			
		||||
//
 | 
			
		||||
//             for (var i = 0; i < samples.Length; i++)
 | 
			
		||||
//             {
 | 
			
		||||
//                 ref var sample = ref samples[i];
 | 
			
		||||
//                 sample = (short) (sample * volume);
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public bool ToggleRepeatSong()
 | 
			
		||||
//         {
 | 
			
		||||
//             lock (locker)
 | 
			
		||||
//             {
 | 
			
		||||
//                 return RepeatCurrentSong = !RepeatCurrentSong;
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public async Task Destroy()
 | 
			
		||||
//         {
 | 
			
		||||
//             Log.Information("Destroying");
 | 
			
		||||
//             lock (locker)
 | 
			
		||||
//             {
 | 
			
		||||
//                 Stop();
 | 
			
		||||
//                 Exited = true;
 | 
			
		||||
//                 Unpause();
 | 
			
		||||
//
 | 
			
		||||
//                 OnCompleted = null;
 | 
			
		||||
//                 OnPauseChanged = null;
 | 
			
		||||
//                 OnStarted = null;
 | 
			
		||||
//             }
 | 
			
		||||
//             var ac = _audioClient;
 | 
			
		||||
//             if (ac != null)
 | 
			
		||||
//                 await ac.StopAsync().ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         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().ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public async Task UpdateSongDurationsAsync()
 | 
			
		||||
//         {
 | 
			
		||||
//             var (_, songs) = Queue.ToArray();
 | 
			
		||||
//             var toUpdate = songs
 | 
			
		||||
//                 .Where(x => x.ProviderType == MusicType.YouTube
 | 
			
		||||
//                     && x.TotalTime == TimeSpan.Zero);
 | 
			
		||||
//
 | 
			
		||||
//             var vIds = toUpdate.Select(x => x.VideoId);
 | 
			
		||||
//             if (!vIds.Any())
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             var durations = await _google.GetVideoDurationsAsync(vIds).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             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);
 | 
			
		||||
//
 | 
			
		||||
//         public void SetMusicChannelToOriginal()
 | 
			
		||||
//         {
 | 
			
		||||
//             this.OutputTextChannel = OriginalTextChannel;
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         //// 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));
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
							
								
								
									
										223
									
								
								src/NadekoBot/Modules/Music/Common/_OldMusicQueue.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/NadekoBot/Modules/Music/Common/_OldMusicQueue.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,223 @@
 | 
			
		||||
// 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 sealed 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 List<SongInfo> ToList()
 | 
			
		||||
//         {
 | 
			
		||||
//             lock (locker)
 | 
			
		||||
//             {
 | 
			
		||||
//                 return Songs.ToList();
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         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
 | 
			
		||||
							
								
								
									
										783
									
								
								src/NadekoBot/Modules/Music/Music.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										783
									
								
								src/NadekoBot/Modules/Music/Music.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,783 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using NadekoBot.Modules.Music.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    [NoPublicBot]
 | 
			
		||||
    public sealed partial class Music : NadekoModule<IMusicService>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly LogCommandService _logService;
 | 
			
		||||
 | 
			
		||||
        public Music(LogCommandService _logService)
 | 
			
		||||
        {
 | 
			
		||||
            this._logService = _logService;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private async Task<bool> ValidateAsync()
 | 
			
		||||
        {
 | 
			
		||||
            var user = (IGuildUser) ctx.User;
 | 
			
		||||
            var userVoiceChannelId = user.VoiceChannel?.Id;
 | 
			
		||||
            
 | 
			
		||||
            if (userVoiceChannelId is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("must_be_in_voice");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var currentUser = await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
            if (currentUser.VoiceChannel?.Id != userVoiceChannelId)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("not_with_bot_in_voice");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static readonly SemaphoreSlim voiceChannelLock = new SemaphoreSlim(1, 1);
 | 
			
		||||
        private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null)
 | 
			
		||||
        {
 | 
			
		||||
            botUser ??= await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
            await voiceChannelLock.WaitAsync();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(Context.Guild.Id, out _))
 | 
			
		||||
                    await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                voiceChannelLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private async Task<bool> QueuePreconditionInternalAsync()
 | 
			
		||||
        {
 | 
			
		||||
            var user = (IGuildUser) Context.User;
 | 
			
		||||
            var voiceChannelId = user.VoiceChannel?.Id;
 | 
			
		||||
            
 | 
			
		||||
            if (voiceChannelId is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("must_be_in_voice");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
            
 | 
			
		||||
            var botUser = await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
            await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
 | 
			
		||||
            
 | 
			
		||||
            if (botUser.VoiceChannel?.Id != voiceChannelId)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("not_with_bot_in_voice");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task QueueByQuery(string query, bool asNext = false, MusicPlatform? forcePlatform = null)
 | 
			
		||||
        {
 | 
			
		||||
            var succ = await QueuePreconditionInternalAsync();
 | 
			
		||||
            if (!succ)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel);
 | 
			
		||||
            if (mp is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var (trackInfo, index) = await mp.TryEnqueueTrackAsync(query, 
 | 
			
		||||
                Context.User.ToString(),
 | 
			
		||||
                asNext,
 | 
			
		||||
                forcePlatform);
 | 
			
		||||
            if (trackInfo is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("song_not_found");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index + 1)).WithMusicIcon())
 | 
			
		||||
                    .WithDescription($"{trackInfo.PrettyName()}\n{GetText("queue")} ")
 | 
			
		||||
                    .WithFooter(ef => ef.WithText(trackInfo.Platform.ToString()));
 | 
			
		||||
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail))
 | 
			
		||||
                    embed.WithThumbnailUrl(trackInfo.Thumbnail);
 | 
			
		||||
 | 
			
		||||
                var queuedMessage = await _service.SendToOutputAsync(Context.Guild.Id, embed).ConfigureAwait(false);
 | 
			
		||||
                queuedMessage?.DeleteAfter(10, _logService);
 | 
			
		||||
                if (mp.IsStopped)
 | 
			
		||||
                {
 | 
			
		||||
                    var msg = await ReplyErrorLocalizedAsync("queue_stopped", Format.Code(Prefix + "play"));
 | 
			
		||||
                    msg.DeleteAfter(10, _logService);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                // ignored
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task MoveToIndex(int index)
 | 
			
		||||
        {
 | 
			
		||||
            if (--index < 0)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            var succ = await QueuePreconditionInternalAsync();
 | 
			
		||||
            if (!succ)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel);
 | 
			
		||||
            if (mp is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            mp.MoveTo(index);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // join vc
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Join()
 | 
			
		||||
        {
 | 
			
		||||
            var user = (IGuildUser) Context.User;
 | 
			
		||||
 | 
			
		||||
            var voiceChannelId = user.VoiceChannel?.Id;
 | 
			
		||||
 | 
			
		||||
            if (voiceChannelId is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("must_be_in_voice");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _service.JoinVoiceChannelAsync(user.GuildId, voiceChannelId.Value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // leave vc (destroy)
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Destroy()
 | 
			
		||||
        {
 | 
			
		||||
            var valid = await ValidateAsync();
 | 
			
		||||
            if (!valid)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await _service.LeaveVoiceChannelAsync(Context.Guild.Id);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // play - no args = next
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(2)]
 | 
			
		||||
        public Task Play()
 | 
			
		||||
            => Next();
 | 
			
		||||
        
 | 
			
		||||
        // play - index = skip to that index
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task Play(int index)
 | 
			
		||||
            => MoveToIndex(index);
 | 
			
		||||
        
 | 
			
		||||
        // play - query = q(query)
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task Play([Leftover] string query)
 | 
			
		||||
            => QueueByQuery(query); 
 | 
			
		||||
        
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public Task Queue([Leftover] string query)
 | 
			
		||||
            => QueueByQuery(query);
 | 
			
		||||
        
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public Task QueueNext([Leftover] string query)
 | 
			
		||||
            => QueueByQuery(query, asNext: true);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Volume(int vol)
 | 
			
		||||
        {
 | 
			
		||||
            if (vol < 0 || vol > 100)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("volume_input_invalid");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var valid = await ValidateAsync();
 | 
			
		||||
            if (!valid)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await _service.SetVolumeAsync(ctx.Guild.Id, vol);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("volume_set", vol);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Next()
 | 
			
		||||
        {
 | 
			
		||||
            var valid = await ValidateAsync();
 | 
			
		||||
            if (!valid)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var success = await _service.PlayAsync(Context.Guild.Id, ((IGuildUser)Context.User).VoiceChannel.Id);
 | 
			
		||||
            if (!success)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private const int LQ_ITEMS_PER_PAGE = 9;
 | 
			
		||||
        
 | 
			
		||||
        // list queue, relevant page
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task ListQueue()
 | 
			
		||||
        {
 | 
			
		||||
            // show page with the current song
 | 
			
		||||
            if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            await ListQueue(mp.CurrentIndex / LQ_ITEMS_PER_PAGE + 1);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // list queue, specify page
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task ListQueue(int page)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            IReadOnlyCollection<IQueuedTrackInfo> tracks;
 | 
			
		||||
            if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || (tracks = mp.GetQueuedTracks()).Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            EmbedBuilder printAction(int curPage)
 | 
			
		||||
            {
 | 
			
		||||
                string desc = string.Empty;
 | 
			
		||||
                var current = mp.GetCurrentTrack(out var currentIndex);
 | 
			
		||||
                if (!(current is null))
 | 
			
		||||
                {
 | 
			
		||||
                    desc = $"`🔊` {current.PrettyFullName()}\n\n" + desc;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var repeatType = mp.Repeat;
 | 
			
		||||
                var add = "";
 | 
			
		||||
                if (mp.IsStopped)
 | 
			
		||||
                    add += Format.Bold(GetText("queue_stopped", Format.Code(Prefix + "play"))) + "\n";
 | 
			
		||||
                 // var mps = mp.MaxPlaytimeSeconds;
 | 
			
		||||
                 // if (mps > 0)
 | 
			
		||||
                 //     add += Format.Bold(GetText("song_skips_after", TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss"))) + "\n";
 | 
			
		||||
                 if (repeatType == PlayerRepeatType.Track)
 | 
			
		||||
                 {
 | 
			
		||||
                     add += "🔂 " + GetText("repeating_track") + "\n";
 | 
			
		||||
                 }
 | 
			
		||||
                 else
 | 
			
		||||
                 {
 | 
			
		||||
                     // if (mp.Autoplay)
 | 
			
		||||
                     //     add += "↪ " + GetText("autoplaying") + "\n";
 | 
			
		||||
                     // if (mp.FairPlay && !mp.Autoplay)
 | 
			
		||||
                     //     add += " " + GetText("fairplay") + "\n";
 | 
			
		||||
                     if (repeatType == PlayerRepeatType.Queue)
 | 
			
		||||
                         add += "🔁 " + GetText("repeating_queue") + "\n";
 | 
			
		||||
                 }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                desc += tracks
 | 
			
		||||
                    .Skip(LQ_ITEMS_PER_PAGE * curPage)
 | 
			
		||||
                    .Take(LQ_ITEMS_PER_PAGE)
 | 
			
		||||
                    .Select((v, index) =>
 | 
			
		||||
                    {
 | 
			
		||||
                        index += LQ_ITEMS_PER_PAGE * curPage;
 | 
			
		||||
                        if (index == currentIndex)
 | 
			
		||||
                             return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
 | 
			
		||||
                         
 | 
			
		||||
                        return $"`{index + 1}.` {v.PrettyFullName()}";
 | 
			
		||||
                     })
 | 
			
		||||
                    .JoinWith('\n');
 | 
			
		||||
                 
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(add))
 | 
			
		||||
                    desc = add + "\n" + desc;
 | 
			
		||||
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithAuthor(eab => eab
 | 
			
		||||
                        .WithName(GetText("player_queue", curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1))
 | 
			
		||||
                        .WithMusicIcon())
 | 
			
		||||
                    .WithDescription(desc)
 | 
			
		||||
                    .WithFooter($"  {mp.PrettyVolume()}  |  🎶 {tracks.Count}  |  ⌛ {mp.PrettyTotalTime()}  ")
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
 | 
			
		||||
                return embed;
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(
 | 
			
		||||
                page,
 | 
			
		||||
                printAction,
 | 
			
		||||
                tracks.Count,
 | 
			
		||||
                LQ_ITEMS_PER_PAGE,
 | 
			
		||||
                false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // search
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task QueueSearch([Leftover] string query)
 | 
			
		||||
        {
 | 
			
		||||
            _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
            var videos = await _service.SearchVideosAsync(query);
 | 
			
		||||
 | 
			
		||||
            if (videos is null || videos.Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("song_not_found").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var resultsString = videos
 | 
			
		||||
                .Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Title)}\n\t{x.Url}")
 | 
			
		||||
                .JoinWith('\n');
 | 
			
		||||
            
 | 
			
		||||
            var msg = await ctx.Channel.SendConfirmAsync(resultsString);
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id).ConfigureAwait(false);
 | 
			
		||||
                if (input == null
 | 
			
		||||
                    || !int.TryParse(input, out var index)
 | 
			
		||||
                    || (index -= 1) < 0
 | 
			
		||||
                    || index >= videos.Count)
 | 
			
		||||
                {
 | 
			
		||||
                    _logService.AddDeleteIgnore(msg.Id);
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await msg.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch
 | 
			
		||||
                    {
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                query = videos[index].Url;
 | 
			
		||||
 | 
			
		||||
                await Play(query);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _logService.AddDeleteIgnore(msg.Id);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await msg.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task TrackRemove(int index)
 | 
			
		||||
        {
 | 
			
		||||
            if (index < 1)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("removed_song_error").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var valid = await ValidateAsync();
 | 
			
		||||
            if (!valid)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (!mp.TryRemoveTrackAt(index - 1, out var song))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("removed_song_error").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var embed = new EmbedBuilder()
 | 
			
		||||
                .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index)).WithMusicIcon())
 | 
			
		||||
                .WithDescription(song.PrettyName())
 | 
			
		||||
                .WithFooter(ef => ef.WithText(song.PrettyInfo()))
 | 
			
		||||
                .WithErrorColor();
 | 
			
		||||
 | 
			
		||||
            await _service.SendToOutputAsync(Context.Guild.Id, embed);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
         public enum All { All = -1 }
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         [Priority(0)]
 | 
			
		||||
         public async Task TrackRemove(All _ = All.All)
 | 
			
		||||
         {
 | 
			
		||||
             var valid = await ValidateAsync();
 | 
			
		||||
             if (!valid)
 | 
			
		||||
                 return;
 | 
			
		||||
 | 
			
		||||
             if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
             
 | 
			
		||||
             mp.Clear();
 | 
			
		||||
             await ReplyConfirmLocalizedAsync("queue_cleared").ConfigureAwait(false);
 | 
			
		||||
         }
 | 
			
		||||
         
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public async Task Stop()
 | 
			
		||||
         {
 | 
			
		||||
             var valid = await ValidateAsync();
 | 
			
		||||
             if (!valid)
 | 
			
		||||
                 return;
 | 
			
		||||
 | 
			
		||||
             if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
             
 | 
			
		||||
             mp.Stop();
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         public enum InputRepeatType
 | 
			
		||||
         {
 | 
			
		||||
             N = 0, No = 0, None = 0,
 | 
			
		||||
             T = 1, Track = 1, S = 1, Song = 1,
 | 
			
		||||
             Q = 2, Queue = 2, Playlist = 2, Pl = 2,
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         private PlayerRepeatType InputToDbType(InputRepeatType type) => type switch
 | 
			
		||||
         {
 | 
			
		||||
             InputRepeatType.None => PlayerRepeatType.None,
 | 
			
		||||
             InputRepeatType.Queue => PlayerRepeatType.Queue,
 | 
			
		||||
             InputRepeatType.Track => PlayerRepeatType.Track,
 | 
			
		||||
             _ => PlayerRepeatType.Queue
 | 
			
		||||
         };
 | 
			
		||||
         
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public async Task QueueRepeat(InputRepeatType type = InputRepeatType.Queue)
 | 
			
		||||
         {
 | 
			
		||||
             var valid = await ValidateAsync();
 | 
			
		||||
             if (!valid)
 | 
			
		||||
                 return;
 | 
			
		||||
             
 | 
			
		||||
             await _service.SetRepeatAsync(ctx.Guild.Id, InputToDbType(type));
 | 
			
		||||
 | 
			
		||||
             if (type == InputRepeatType.None)
 | 
			
		||||
                 await ReplyConfirmLocalizedAsync("repeating_none");
 | 
			
		||||
             else if (type == InputRepeatType.Queue)
 | 
			
		||||
                 await ReplyConfirmLocalizedAsync("repeating_queue");
 | 
			
		||||
             else
 | 
			
		||||
                 await ReplyConfirmLocalizedAsync("repeating_track");
 | 
			
		||||
         }
 | 
			
		||||
         
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public async Task ReptCurSong()
 | 
			
		||||
         {
 | 
			
		||||
             await ReplyPendingLocalizedAsync("obsolete_use", $"`{Prefix}qrp song`");
 | 
			
		||||
             await QueueRepeat(InputRepeatType.Song);
 | 
			
		||||
         }
 | 
			
		||||
         
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public async Task Pause()
 | 
			
		||||
         {
 | 
			
		||||
             var valid = await ValidateAsync();
 | 
			
		||||
             if (!valid)
 | 
			
		||||
                 return;
 | 
			
		||||
 | 
			
		||||
             if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || mp.GetCurrentTrack(out _) is null)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
             mp.TogglePause();
 | 
			
		||||
         }
 | 
			
		||||
         
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public Task Radio(string radioLink)
 | 
			
		||||
             => QueueByQuery(radioLink, false, MusicPlatform.Radio);
 | 
			
		||||
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         [OwnerOnly]
 | 
			
		||||
         public Task Local([Leftover] string path)
 | 
			
		||||
             => QueueByQuery(path, false, MusicPlatform.Local);
 | 
			
		||||
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         [OwnerOnly]
 | 
			
		||||
         public async Task LocalPlaylist([Leftover] string dirPath)
 | 
			
		||||
         {
 | 
			
		||||
             if (string.IsNullOrWhiteSpace(dirPath))
 | 
			
		||||
                 return;
 | 
			
		||||
 | 
			
		||||
             var user = (IGuildUser) Context.User;
 | 
			
		||||
             var voiceChannelId = user.VoiceChannel?.Id;
 | 
			
		||||
        
 | 
			
		||||
             if (voiceChannelId is null)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("must_be_in_voice");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
             _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
        
 | 
			
		||||
             var botUser = await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
             await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
 | 
			
		||||
        
 | 
			
		||||
             if (botUser.VoiceChannel?.Id != voiceChannelId)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("not_with_bot_in_voice");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
            
 | 
			
		||||
             var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel);
 | 
			
		||||
             if (mp is null)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
             
 | 
			
		||||
             await _service.EnqueueDirectoryAsync(mp, dirPath, ctx.User.ToString());
 | 
			
		||||
             
 | 
			
		||||
             await ReplyConfirmLocalizedAsync("dir_queue_complete").ConfigureAwait(false);
 | 
			
		||||
         }
 | 
			
		||||
         
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public async Task MoveSong(int from, int to)
 | 
			
		||||
         {
 | 
			
		||||
             if (--from < 0 || --to < 0 || from == to)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("invalid_input").ConfigureAwait(false);
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
             var valid = await ValidateAsync();
 | 
			
		||||
             if (!valid)
 | 
			
		||||
                 return;
 | 
			
		||||
             
 | 
			
		||||
             var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel);
 | 
			
		||||
             if (mp is null)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
             var track = mp.MoveTrack(from, to);
 | 
			
		||||
             if (track is null)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("invalid_input").ConfigureAwait(false);
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
             
 | 
			
		||||
             var embed = new EmbedBuilder()
 | 
			
		||||
                 .WithTitle(track.Title.TrimTo(65))
 | 
			
		||||
                 .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png"))
 | 
			
		||||
                 .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{from + 1}").WithIsInline(true))
 | 
			
		||||
                 .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{to + 1}").WithIsInline(true))
 | 
			
		||||
                 .WithColor(NadekoBot.OkColor);
 | 
			
		||||
 | 
			
		||||
             if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
 | 
			
		||||
                 embed.WithUrl(track.Url);
 | 
			
		||||
 | 
			
		||||
             await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public Task SoundCloudQueue([Leftover] string query)
 | 
			
		||||
             => QueueByQuery(query, false, MusicPlatform.SoundCloud);
 | 
			
		||||
         
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public async Task SoundCloudPl([Leftover] string playlist)
 | 
			
		||||
         {
 | 
			
		||||
             if (string.IsNullOrWhiteSpace(playlist))
 | 
			
		||||
                 return;
 | 
			
		||||
 | 
			
		||||
             var succ = await QueuePreconditionInternalAsync();
 | 
			
		||||
             if (!succ)
 | 
			
		||||
                 return;
 | 
			
		||||
 | 
			
		||||
             var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel);
 | 
			
		||||
             if (mp is null)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
             
 | 
			
		||||
             _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
             await _service.EnqueueSoundcloudPlaylistAsync(mp, playlist, ctx.User.ToString());
 | 
			
		||||
 | 
			
		||||
             await ctx.OkAsync();
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public async Task Playlist([Leftover] string playlistQuery)
 | 
			
		||||
         {
 | 
			
		||||
             if (string.IsNullOrWhiteSpace(playlistQuery))
 | 
			
		||||
                 return;
 | 
			
		||||
 | 
			
		||||
             var succ = await QueuePreconditionInternalAsync();
 | 
			
		||||
             if (!succ)
 | 
			
		||||
                 return;
 | 
			
		||||
 | 
			
		||||
             var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel);
 | 
			
		||||
             if (mp is null)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
             _ = Context.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
             var queuedCount = await _service.EnqueueYoutubePlaylistAsync(mp, playlistQuery, ctx.User.ToString());
 | 
			
		||||
             if (queuedCount == 0)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("no_search_results").ConfigureAwait(false);
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
             await ctx.OkAsync();
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public async Task NowPlaying()
 | 
			
		||||
         {
 | 
			
		||||
             var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel);
 | 
			
		||||
             if (mp is null)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
             var currentTrack = mp.GetCurrentTrack(out _);
 | 
			
		||||
             if (currentTrack == null)
 | 
			
		||||
                 return;
 | 
			
		||||
 | 
			
		||||
             var embed = new EmbedBuilder().WithOkColor()
 | 
			
		||||
                 .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon())
 | 
			
		||||
                 .WithDescription(currentTrack.PrettyName())
 | 
			
		||||
                 .WithThumbnailUrl(currentTrack.Thumbnail)
 | 
			
		||||
                 .WithFooter($"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
 | 
			
		||||
 | 
			
		||||
             await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public async Task PlaylistShuffle()
 | 
			
		||||
         {
 | 
			
		||||
             var valid = await ValidateAsync();
 | 
			
		||||
             if (!valid)
 | 
			
		||||
                 return;
 | 
			
		||||
             
 | 
			
		||||
             var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel);
 | 
			
		||||
             if (mp is null)
 | 
			
		||||
             {
 | 
			
		||||
                 await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                 return;
 | 
			
		||||
             }
 | 
			
		||||
             
 | 
			
		||||
             mp.ShuffleQueue();
 | 
			
		||||
             await ReplyConfirmLocalizedAsync("queue_shuffled");
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
         public async Task SetMusicChannel()
 | 
			
		||||
         {
 | 
			
		||||
             await _service.SetMusicChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
 | 
			
		||||
             await ReplyConfirmLocalizedAsync("set_music_channel");
 | 
			
		||||
         }
 | 
			
		||||
         
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
         public async Task UnsetMusicChannel()
 | 
			
		||||
         {
 | 
			
		||||
             await _service.SetMusicChannelAsync(ctx.Guild.Id, null);
 | 
			
		||||
 | 
			
		||||
             await ReplyConfirmLocalizedAsync("unset_music_channel");
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         public async Task AutoDisconnect()
 | 
			
		||||
         {
 | 
			
		||||
             var newState = await _service.ToggleAutoDisconnectAsync(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
             if(newState)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("autodc_enable");
 | 
			
		||||
             else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("autodc_disable");
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         [RequireUserPermission(GuildPermission.Administrator)]
 | 
			
		||||
         public async Task MusicQuality()
 | 
			
		||||
         {
 | 
			
		||||
             var quality = await _service.GetMusicQualityAsync(ctx.Guild.Id);
 | 
			
		||||
             await ReplyConfirmLocalizedAsync("current_music_quality", Format.Bold(quality.ToString()));
 | 
			
		||||
         }
 | 
			
		||||
         
 | 
			
		||||
         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
         [RequireContext(ContextType.Guild)]
 | 
			
		||||
         [RequireUserPermission(GuildPermission.Administrator)]
 | 
			
		||||
         public async Task MusicQuality(QualityPreset preset)
 | 
			
		||||
         {
 | 
			
		||||
             await _service.SetMusicQualityAsync(ctx.Guild.Id, preset);
 | 
			
		||||
             await ReplyConfirmLocalizedAsync("music_quality_set", Format.Bold(preset.ToString()));
 | 
			
		||||
         }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										247
									
								
								src/NadekoBot/Modules/Music/PlaylistCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								src/NadekoBot/Modules/Music/PlaylistCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,247 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules;
 | 
			
		||||
using NadekoBot.Modules.Music.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Music
 | 
			
		||||
{
 | 
			
		||||
    public sealed partial class Music
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public sealed class PlaylistCommands : NadekoModule<IMusicService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly DbService _db;
 | 
			
		||||
            private readonly IBotCredentials _creds;
 | 
			
		||||
 | 
			
		||||
            public PlaylistCommands(DbService db, IBotCredentials creds)
 | 
			
		||||
            {
 | 
			
		||||
                _db = db;
 | 
			
		||||
                _creds = creds;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null)
 | 
			
		||||
            {
 | 
			
		||||
                botUser ??= await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
                await voiceChannelLock.WaitAsync();
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(Context.Guild.Id, out _))
 | 
			
		||||
                        await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId);
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    voiceChannelLock.Release();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Playlists([Leftover] int num = 1)
 | 
			
		||||
            {
 | 
			
		||||
                if (num <= 0)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                List<MusicPlaylist> playlists;
 | 
			
		||||
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithAuthor(eab => eab.WithName(GetText("playlists_page", num)).WithMusicIcon())
 | 
			
		||||
                    .WithDescription(string.Join("\n", playlists.Select(r =>
 | 
			
		||||
                        GetText("playlists", r.Id, r.Name, r.Author, r.Songs.Count))))
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
                await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task DeletePlaylist([Leftover] int id)
 | 
			
		||||
            {
 | 
			
		||||
                var success = false;
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    using (var uow = _db.GetDbContext())
 | 
			
		||||
                    {
 | 
			
		||||
                        var pl = uow.MusicPlaylists.GetById(id);
 | 
			
		||||
 | 
			
		||||
                        if (pl != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (_creds.IsOwner(ctx.User) || pl.AuthorId == ctx.User.Id)
 | 
			
		||||
                            {
 | 
			
		||||
                                uow.MusicPlaylists.Remove(pl);
 | 
			
		||||
                                await uow.SaveChangesAsync();
 | 
			
		||||
                                success = true;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "Error deleting playlist");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!success)
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("playlist_delete_fail").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("playlist_deleted").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task PlaylistShow(int id, int page = 1)
 | 
			
		||||
            {
 | 
			
		||||
                if (page-- < 1)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                MusicPlaylist mpl;
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    mpl = uow.MusicPlaylists.GetWithSongs(id);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.SendPaginatedConfirmAsync(page, (cur) =>
 | 
			
		||||
                {
 | 
			
		||||
                    var i = 0;
 | 
			
		||||
                    var str = string.Join("\n", mpl.Songs
 | 
			
		||||
                        .Skip(cur * 20)
 | 
			
		||||
                        .Take(20)
 | 
			
		||||
                        .Select(x => $"`{++i}.` [{x.Title.TrimTo(45)}]({x.Query}) `{x.Provider}`"));
 | 
			
		||||
                    return new EmbedBuilder()
 | 
			
		||||
                        .WithTitle($"\"{mpl.Name}\" by {mpl.Author}")
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithDescription(str);
 | 
			
		||||
                }, mpl.Songs.Count, 20).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Save([Leftover] string name)
 | 
			
		||||
            {
 | 
			
		||||
                if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var songs = mp.GetQueuedTracks()
 | 
			
		||||
                    .Select(s => new PlaylistSong()
 | 
			
		||||
                    {
 | 
			
		||||
                        Provider = s.Platform.ToString(),
 | 
			
		||||
                        ProviderType = (MusicType)s.Platform,
 | 
			
		||||
                        Title = s.Title,
 | 
			
		||||
                        Query = s.Url,
 | 
			
		||||
                    }).ToList();
 | 
			
		||||
 | 
			
		||||
                MusicPlaylist playlist;
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    playlist = new MusicPlaylist
 | 
			
		||||
                    {
 | 
			
		||||
                        Name = name,
 | 
			
		||||
                        Author = ctx.User.Username,
 | 
			
		||||
                        AuthorId = ctx.User.Id,
 | 
			
		||||
                        Songs = songs.ToList(),
 | 
			
		||||
                    };
 | 
			
		||||
                    uow.MusicPlaylists.Add(playlist);
 | 
			
		||||
                    await uow.SaveChangesAsync();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                        .WithTitle(GetText("playlist_saved"))
 | 
			
		||||
                        .AddField(efb => efb.WithName(GetText("name")).WithValue(name))
 | 
			
		||||
                        .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString())))
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            private static readonly SemaphoreSlim _playlistLock = new SemaphoreSlim(1, 1);
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Load([Leftover] int id)
 | 
			
		||||
            {
 | 
			
		||||
                // expensive action, 1 at a time
 | 
			
		||||
                await _playlistLock.WaitAsync();
 | 
			
		||||
                try
 | 
			
		||||
                {                    
 | 
			
		||||
                    var user = (IGuildUser) Context.User;
 | 
			
		||||
                    var voiceChannelId = user.VoiceChannel?.Id;
 | 
			
		||||
        
 | 
			
		||||
                    if (voiceChannelId is null)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("must_be_in_voice");
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
        
 | 
			
		||||
                    var botUser = await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
                    await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
 | 
			
		||||
        
 | 
			
		||||
                    if (botUser.VoiceChannel?.Id != voiceChannelId)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("not_with_bot_in_voice");
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
            
 | 
			
		||||
                    var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) Context.Channel);
 | 
			
		||||
                    if (mp is null)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("no_player");
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    MusicPlaylist mpl;
 | 
			
		||||
                    using (var uow = _db.GetDbContext())
 | 
			
		||||
                    {
 | 
			
		||||
                        mpl = uow.MusicPlaylists.GetWithSongs(id);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (mpl == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("playlist_id_not_found").ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    IUserMessage msg = null;
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        msg = await ctx.Channel
 | 
			
		||||
                            .SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString())))
 | 
			
		||||
                            .ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception)
 | 
			
		||||
                    {
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await mp.EnqueueManyAsync(
 | 
			
		||||
                        mpl.Songs.Select(x => (x.Query, (MusicPlatform) x.ProviderType)),
 | 
			
		||||
                        ctx.User.ToString()
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    if (msg != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete"));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    _playlistLock.Release();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										221
									
								
								src/NadekoBot/Modules/Music/Services/AyuVoiceStateService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								src/NadekoBot/Modules/Music/Services/AyuVoiceStateService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,221 @@
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Ayu.Discord.Voice;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Services
 | 
			
		||||
{
 | 
			
		||||
    public sealed class AyuVoiceStateService : INService
 | 
			
		||||
    {
 | 
			
		||||
        // public delegate Task VoiceProxyUpdatedDelegate(ulong guildId, IVoiceProxy proxy);
 | 
			
		||||
        // public event VoiceProxyUpdatedDelegate OnVoiceProxyUpdate = delegate { return Task.CompletedTask; };
 | 
			
		||||
        
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, IVoiceProxy> _voiceProxies = new ConcurrentDictionary<ulong, IVoiceProxy>();
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, SemaphoreSlim> _voiceGatewayLocks = new ConcurrentDictionary<ulong, SemaphoreSlim>();
 | 
			
		||||
        
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly MethodInfo _sendVoiceStateUpdateMethodInfo;
 | 
			
		||||
        private readonly object _dnetApiClient;
 | 
			
		||||
        private readonly ulong _currentUserId;
 | 
			
		||||
 | 
			
		||||
        public AyuVoiceStateService(DiscordSocketClient client)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _currentUserId = _client.CurrentUser.Id;
 | 
			
		||||
 | 
			
		||||
            var prop = _client.GetType()
 | 
			
		||||
                .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
 | 
			
		||||
                .First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient");
 | 
			
		||||
            _dnetApiClient = prop.GetValue(_client, null);
 | 
			
		||||
            _sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType().GetMethod("SendVoiceStateUpdateAsync");
 | 
			
		||||
            
 | 
			
		||||
            _client.LeftGuild += ClientOnLeftGuild;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task ClientOnLeftGuild(SocketGuild guild)
 | 
			
		||||
        {
 | 
			
		||||
            if (_voiceProxies.TryRemove(guild.Id, out var proxy))
 | 
			
		||||
            {
 | 
			
		||||
                proxy.StopGateway();
 | 
			
		||||
                proxy.SetGateway(null);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task InvokeSendVoiceStateUpdateAsync(ulong guildId, ulong? channelId = null, bool isDeafened = false, bool isMuted = false)
 | 
			
		||||
        {
 | 
			
		||||
            // return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted);
 | 
			
		||||
            return (Task) _sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient, new object[] {guildId, channelId, isMuted, isDeafened, null});
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task SendLeaveVoiceChannelInternalAsync(ulong guildId)
 | 
			
		||||
            => InvokeSendVoiceStateUpdateAsync(guildId);
 | 
			
		||||
 | 
			
		||||
        private Task SendJoinVoiceChannelInternalAsync(ulong guildId, ulong channelId)
 | 
			
		||||
            => InvokeSendVoiceStateUpdateAsync(guildId, channelId);
 | 
			
		||||
        
 | 
			
		||||
        private SemaphoreSlim GetVoiceGatewayLock(ulong guildId) => _voiceGatewayLocks.GetOrAdd(guildId, new SemaphoreSlim(1, 1));
 | 
			
		||||
        
 | 
			
		||||
        private async Task LeaveVoiceChannelInternalAsync(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            var complete = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
			
		||||
            Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
 | 
			
		||||
            {
 | 
			
		||||
                if (user is SocketGuildUser guildUser
 | 
			
		||||
                    && guildUser.Guild.Id == guildId
 | 
			
		||||
                    && newState.VoiceChannel?.Id is null)
 | 
			
		||||
                {
 | 
			
		||||
                    complete.TrySetResult(true);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
 | 
			
		||||
 | 
			
		||||
                if (_voiceProxies.TryGetValue(guildId, out var proxy))
 | 
			
		||||
                {
 | 
			
		||||
                    _ = proxy.StopGateway();
 | 
			
		||||
                    proxy.SetGateway(null);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await SendLeaveVoiceChannelInternalAsync(guildId);
 | 
			
		||||
                await Task.WhenAny(Task.Delay(1500), complete.Task);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        public async Task LeaveVoiceChannel(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            var gwLock = GetVoiceGatewayLock(guildId);
 | 
			
		||||
            await gwLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await LeaveVoiceChannelInternalAsync(guildId);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                gwLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<IVoiceProxy> InternalConnectToVcAsync(ulong guildId, ulong channelId)
 | 
			
		||||
        {
 | 
			
		||||
            var voiceStateUpdatedSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
			
		||||
            var voiceServerUpdatedSource = new TaskCompletionSource<SocketVoiceServer>(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
			
		||||
 | 
			
		||||
            Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
 | 
			
		||||
            {
 | 
			
		||||
                if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId)
 | 
			
		||||
                {
 | 
			
		||||
                    if (newState.VoiceChannel?.Id == channelId)
 | 
			
		||||
                        voiceStateUpdatedSource.TrySetResult(newState.VoiceSessionId);
 | 
			
		||||
 | 
			
		||||
                    voiceStateUpdatedSource.TrySetResult(null);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Task OnVoiceServerUpdated(SocketVoiceServer data)
 | 
			
		||||
            {
 | 
			
		||||
                if (data.Guild.Id == guildId)
 | 
			
		||||
                {
 | 
			
		||||
                    voiceServerUpdatedSource.TrySetResult(data);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                _client.VoiceServerUpdated += OnVoiceServerUpdated;
 | 
			
		||||
                _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
 | 
			
		||||
 | 
			
		||||
                await SendJoinVoiceChannelInternalAsync(guildId, channelId);
 | 
			
		||||
 | 
			
		||||
                // create a delay task, how much to wait for gateway response
 | 
			
		||||
                var delayTask = Task.Delay(2500);
 | 
			
		||||
 | 
			
		||||
                // either delay or successful voiceStateUpdate
 | 
			
		||||
                var maybeUpdateTask = Task.WhenAny(delayTask, voiceStateUpdatedSource.Task);
 | 
			
		||||
                // either delay or successful voiceServerUpdate
 | 
			
		||||
                var maybeServerTask = Task.WhenAny(delayTask, voiceServerUpdatedSource.Task);
 | 
			
		||||
 | 
			
		||||
                // wait for both to end (max 1s) and check if either of them is a delay task
 | 
			
		||||
                var results = await Task.WhenAll(maybeUpdateTask, maybeServerTask);
 | 
			
		||||
                if (results[0] == delayTask || results[1] == delayTask)
 | 
			
		||||
                {
 | 
			
		||||
                    // if either is delay, return null - connection unsuccessful
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // if both are succesful, that means we can safely get
 | 
			
		||||
                // the values from  completion sources
 | 
			
		||||
 | 
			
		||||
                var session = await voiceStateUpdatedSource.Task;
 | 
			
		||||
 | 
			
		||||
                // session can be null. Means we disconnected, or connected to the wrong channel (?!)
 | 
			
		||||
                if (session is null)
 | 
			
		||||
                    return null;
 | 
			
		||||
 | 
			
		||||
                var voiceServerData = await voiceServerUpdatedSource.Task;
 | 
			
		||||
                
 | 
			
		||||
                VoiceGateway CreateVoiceGatewayLocal() =>
 | 
			
		||||
                    new VoiceGateway(
 | 
			
		||||
                        guildId,
 | 
			
		||||
                        _currentUserId,
 | 
			
		||||
                        session,
 | 
			
		||||
                        voiceServerData.Token,
 | 
			
		||||
                        voiceServerData.Endpoint
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                var current = _voiceProxies.AddOrUpdate(
 | 
			
		||||
                    guildId,
 | 
			
		||||
                    (gid) => new VoiceProxy(CreateVoiceGatewayLocal()),
 | 
			
		||||
                    (gid, currentProxy) =>
 | 
			
		||||
                    {
 | 
			
		||||
                        _ = currentProxy.StopGateway();
 | 
			
		||||
                        currentProxy.SetGateway(CreateVoiceGatewayLocal());
 | 
			
		||||
                        return currentProxy;
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                _ = current.StartGateway(); // don't await, this blocks until gateway is closed
 | 
			
		||||
                return current;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _client.VoiceServerUpdated -= OnVoiceServerUpdated;
 | 
			
		||||
                _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<IVoiceProxy> JoinVoiceChannel(ulong guildId, ulong channelId, bool forceReconnect = true)
 | 
			
		||||
        {
 | 
			
		||||
            var gwLock = GetVoiceGatewayLock(guildId);
 | 
			
		||||
            await gwLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await LeaveVoiceChannelInternalAsync(guildId);
 | 
			
		||||
                return await InternalConnectToVcAsync(guildId, channelId);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                gwLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy)
 | 
			
		||||
            => _voiceProxies.TryGetValue(guildId, out proxy);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/NadekoBot/Modules/Music/Services/IMusicService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/NadekoBot/Modules/Music/Services/IMusicService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Music;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IMusicService : IPlaceholderProvider
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Leave voice channel in the specified guild if it's connected to one
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="guildId">Id of the guild</param>
 | 
			
		||||
        public Task LeaveVoiceChannelAsync(ulong guildId);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Joins the voice channel with the specified id
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="guildId">Id of the guild where the voice channel is</param>
 | 
			
		||||
        /// <param name="voiceChannelId">Id of the voice channel</param>
 | 
			
		||||
        public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId);
 | 
			
		||||
        
 | 
			
		||||
        Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel);
 | 
			
		||||
        bool TryGetMusicPlayer(ulong guildId, out IMusicPlayer musicPlayer);
 | 
			
		||||
        Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string playlistId, string queuer);
 | 
			
		||||
        Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer);
 | 
			
		||||
        Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer);
 | 
			
		||||
        Task<IUserMessage?> SendToOutputAsync(ulong guildId, EmbedBuilder embed);
 | 
			
		||||
        Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId);
 | 
			
		||||
        Task<IList<(string Title, string Url)>> SearchVideosAsync(string query);
 | 
			
		||||
        Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId);
 | 
			
		||||
        Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType);
 | 
			
		||||
        Task SetVolumeAsync(ulong guildId, int value);
 | 
			
		||||
        Task<bool> ToggleAutoDisconnectAsync(ulong guildId);
 | 
			
		||||
        Task<QualityPreset> GetMusicQualityAsync(ulong guildId);
 | 
			
		||||
        Task SetMusicQualityAsync(ulong guildId, QualityPreset preset);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										457
									
								
								src/NadekoBot/Modules/Music/Services/MusicService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										457
									
								
								src/NadekoBot/Modules/Music/Services/MusicService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,457 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Core.Modules.Music;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Repositories.Impl;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Services
 | 
			
		||||
{
 | 
			
		||||
    public sealed class MusicService : IMusicService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly AyuVoiceStateService _voiceStateService;
 | 
			
		||||
        private readonly ITrackResolveProvider _trackResolveProvider;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly IYoutubeResolver _ytResolver;
 | 
			
		||||
        private readonly ILocalTrackResolver _localResolver;
 | 
			
		||||
        private readonly ISoundcloudResolver _scResolver;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly IBotStrings _strings;
 | 
			
		||||
        private readonly IGoogleApiService _googleApiService;
 | 
			
		||||
        private readonly YtLoader _ytLoader;
 | 
			
		||||
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, IMusicPlayer> _players;
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, (ITextChannel Default, ITextChannel? Override)> _outputChannels;
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, MusicPlayerSettings> _settings;
 | 
			
		||||
 | 
			
		||||
        public MusicService(AyuVoiceStateService voiceStateService, ITrackResolveProvider trackResolveProvider,
 | 
			
		||||
            DbService db, IYoutubeResolver ytResolver, ILocalTrackResolver localResolver, ISoundcloudResolver scResolver,
 | 
			
		||||
            DiscordSocketClient client, IBotStrings strings, IGoogleApiService googleApiService, YtLoader ytLoader)
 | 
			
		||||
        {
 | 
			
		||||
            _voiceStateService = voiceStateService;
 | 
			
		||||
            _trackResolveProvider = trackResolveProvider;
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _ytResolver = ytResolver;
 | 
			
		||||
            _localResolver = localResolver;
 | 
			
		||||
            _scResolver = scResolver;
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _strings = strings;
 | 
			
		||||
            _googleApiService = googleApiService;
 | 
			
		||||
            _ytLoader = ytLoader;
 | 
			
		||||
 | 
			
		||||
            _players = new ConcurrentDictionary<ulong, IMusicPlayer>();
 | 
			
		||||
            _outputChannels = new ConcurrentDictionary<ulong, (ITextChannel, ITextChannel?)>();
 | 
			
		||||
            _settings = new ConcurrentDictionary<ulong, MusicPlayerSettings>();
 | 
			
		||||
            
 | 
			
		||||
            _client.LeftGuild += ClientOnLeftGuild;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private void DisposeMusicPlayer(IMusicPlayer musicPlayer)
 | 
			
		||||
        {
 | 
			
		||||
            musicPlayer.Kill();
 | 
			
		||||
            _ = Task.Delay(10_000).ContinueWith(_ => musicPlayer.Dispose());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void RemoveMusicPlayer(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            _outputChannels.TryRemove(guildId, out _);
 | 
			
		||||
            if (_players.TryRemove(guildId, out var mp))
 | 
			
		||||
            {
 | 
			
		||||
                DisposeMusicPlayer(mp);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task ClientOnLeftGuild(SocketGuild guild)
 | 
			
		||||
        {
 | 
			
		||||
            RemoveMusicPlayer(guild.Id);
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task LeaveVoiceChannelAsync(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            RemoveMusicPlayer(guildId);
 | 
			
		||||
            await _voiceStateService.LeaveVoiceChannel(guildId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId) 
 | 
			
		||||
            => _voiceStateService.JoinVoiceChannel(guildId, voiceChannelId);
 | 
			
		||||
 | 
			
		||||
        public async Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel)
 | 
			
		||||
        {
 | 
			
		||||
            var newPLayer = await CreateMusicPlayerInternalAsync(contextChannel.GuildId, contextChannel);
 | 
			
		||||
            if (newPLayer is null)
 | 
			
		||||
                return null;
 | 
			
		||||
            
 | 
			
		||||
            return _players.GetOrAdd(contextChannel.GuildId, newPLayer);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool TryGetMusicPlayer(ulong guildId, out IMusicPlayer musicPlayer)
 | 
			
		||||
            => _players.TryGetValue(guildId, out musicPlayer);
 | 
			
		||||
 | 
			
		||||
        public async Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer)
 | 
			
		||||
        {
 | 
			
		||||
            var count = 0;
 | 
			
		||||
            await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query))
 | 
			
		||||
            {
 | 
			
		||||
                if (mp.IsKilled)
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                mp.EnqueueTrack(track, queuer);
 | 
			
		||||
                ++count;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return count;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer)
 | 
			
		||||
        {
 | 
			
		||||
            await foreach (var track in _localResolver.ResolveDirectoryAsync(dirPath))
 | 
			
		||||
            {
 | 
			
		||||
                if (mp.IsKilled)
 | 
			
		||||
                    break;
 | 
			
		||||
                
 | 
			
		||||
                mp.EnqueueTrack(track, queuer);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer)
 | 
			
		||||
        {
 | 
			
		||||
            var i = 0;
 | 
			
		||||
            await foreach (var track in _scResolver.ResolvePlaylistAsync(playlist))
 | 
			
		||||
            {
 | 
			
		||||
                if (mp.IsKilled)
 | 
			
		||||
                    break;
 | 
			
		||||
                
 | 
			
		||||
                mp.EnqueueTrack(track, queuer);
 | 
			
		||||
                ++i;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return i;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<IMusicPlayer?> CreateMusicPlayerInternalAsync(ulong guildId, ITextChannel defaultChannel)
 | 
			
		||||
        {
 | 
			
		||||
            var queue = new MusicQueue();
 | 
			
		||||
            var resolver = _trackResolveProvider;
 | 
			
		||||
 | 
			
		||||
            if (!_voiceStateService.TryGetProxy(guildId, out var proxy))
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var settings = await GetSettingsInternalAsync(guildId);
 | 
			
		||||
 | 
			
		||||
            ITextChannel? overrideChannel = null;
 | 
			
		||||
            if (settings.MusicChannelId is ulong channelId)
 | 
			
		||||
            {
 | 
			
		||||
                overrideChannel = _client.GetGuild(guildId)?.GetTextChannel(channelId);
 | 
			
		||||
 | 
			
		||||
                if (overrideChannel is null)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning("Saved music output channel doesn't exist, falling back to current channel");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            _outputChannels[guildId] = (defaultChannel, overrideChannel);
 | 
			
		||||
 | 
			
		||||
            var mp = new MusicPlayer(
 | 
			
		||||
                queue,
 | 
			
		||||
                resolver,
 | 
			
		||||
                proxy,
 | 
			
		||||
                settings.QualityPreset
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            mp.SetRepeat(settings.PlayerRepeat);
 | 
			
		||||
 | 
			
		||||
            if (settings.Volume >= 0 && settings.Volume <= 100)
 | 
			
		||||
            {
 | 
			
		||||
                mp.SetVolume(settings.Volume);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error("Saved Volume is outside of valid range >= 0 && <=100 ({Volume})", settings.Volume);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            mp.OnCompleted += OnTrackCompleted(guildId);
 | 
			
		||||
            mp.OnStarted += OnTrackStarted(guildId);
 | 
			
		||||
            mp.OnQueueStopped += OnQueueStopped(guildId);
 | 
			
		||||
 | 
			
		||||
            return mp;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<IUserMessage?> SendToOutputAsync(ulong guildId, EmbedBuilder embed)
 | 
			
		||||
        {
 | 
			
		||||
            if (_outputChannels.TryGetValue(guildId, out var chan))
 | 
			
		||||
                return (chan.Default ?? chan.Override).EmbedAsync(embed);
 | 
			
		||||
 | 
			
		||||
            return Task.FromResult<IUserMessage?>(null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Func<IMusicPlayer, IQueuedTrackInfo, Task> OnTrackCompleted(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            IUserMessage? lastFinishedMessage = null;
 | 
			
		||||
            return async (mp, trackInfo) =>
 | 
			
		||||
            {
 | 
			
		||||
                _ = lastFinishedMessage?.DeleteAsync();
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithAuthor(eab => eab.WithName(GetText(guildId, "finished_song")).WithMusicIcon())
 | 
			
		||||
                    .WithDescription(trackInfo.PrettyName())
 | 
			
		||||
                    .WithFooter(trackInfo.PrettyTotalTime());
 | 
			
		||||
 | 
			
		||||
                lastFinishedMessage = await SendToOutputAsync(guildId, embed);
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Func<IMusicPlayer, IQueuedTrackInfo, int, Task> OnTrackStarted(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            IUserMessage? lastPlayingMessage = null;
 | 
			
		||||
            return async (mp, trackInfo, index) =>
 | 
			
		||||
            {
 | 
			
		||||
                _ = lastPlayingMessage?.DeleteAsync();
 | 
			
		||||
                var embed = new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithAuthor(eab => eab.WithName(GetText(guildId, "playing_song", index + 1)).WithMusicIcon())
 | 
			
		||||
                    .WithDescription(trackInfo.PrettyName())
 | 
			
		||||
                    .WithFooter(ef => ef.WithText($"{mp.PrettyVolume()} | {trackInfo.PrettyInfo()}"));
 | 
			
		||||
 | 
			
		||||
                lastPlayingMessage = await SendToOutputAsync(guildId, embed);
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Func<IMusicPlayer, Task> OnQueueStopped(ulong guildId)
 | 
			
		||||
            => (mp) =>
 | 
			
		||||
            {
 | 
			
		||||
                if (_settings.TryGetValue(guildId, out var settings))
 | 
			
		||||
                {
 | 
			
		||||
                    if (settings.AutoDisconnect)
 | 
			
		||||
                    {
 | 
			
		||||
                        return LeaveVoiceChannelAsync(guildId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        // this has to be done because dragging bot to another vc isn't supported yet
 | 
			
		||||
        public async Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId)
 | 
			
		||||
        {
 | 
			
		||||
            if (!TryGetMusicPlayer(guildId, out var mp))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (mp.IsStopped)
 | 
			
		||||
            {
 | 
			
		||||
                if (!_voiceStateService.TryGetProxy(guildId, out var proxy) 
 | 
			
		||||
                    || proxy.State == VoiceProxy.VoiceProxyState.Stopped)
 | 
			
		||||
                {
 | 
			
		||||
                    await JoinVoiceChannelAsync(guildId, voiceChannelId);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            mp.Next();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<IList<(string Title, string Url)>> SearchYtLoaderVideosAsync(string query)
 | 
			
		||||
        {
 | 
			
		||||
            var result = await _ytLoader.LoadResultsAsync(query);
 | 
			
		||||
            return result.Select(x => (x.Title, x.Url)).ToList();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private async Task<IList<(string Title, string Url)>> SearchGoogleApiVideosAsync(string query)
 | 
			
		||||
        {
 | 
			
		||||
            var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5);
 | 
			
		||||
            return result.Select(x => (x.Name, x.Url)).ToList();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public async Task<IList<(string Title, string Url)>> SearchVideosAsync(string query)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                IList<(string, string)> videos = await SearchYtLoaderVideosAsync(query);
 | 
			
		||||
                if (videos.Count > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    return videos;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Failed geting videos with YtLoader: {ErrorMessage}", ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                return await SearchGoogleApiVideosAsync(query);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Failed getting video results with Google Api. " +
 | 
			
		||||
                            "Probably google api key missing: {ErrorMessage}", ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            return Array.Empty<(string, string)>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GetText(ulong guildId, string key, params object[] args)
 | 
			
		||||
            => _strings.GetText(key, guildId, args);
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders()
 | 
			
		||||
        {
 | 
			
		||||
            // random song that's playing
 | 
			
		||||
            yield return ("%music.playing%", () =>
 | 
			
		||||
            {
 | 
			
		||||
                var randomPlayingTrack = _players
 | 
			
		||||
                    .Select(x => x.Value.GetCurrentTrack(out _))
 | 
			
		||||
                    .Where(x => !(x is null))
 | 
			
		||||
                    .Shuffle()
 | 
			
		||||
                    .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
                if (randomPlayingTrack is null)
 | 
			
		||||
                    return "-";
 | 
			
		||||
 | 
			
		||||
                return randomPlayingTrack.Title;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // number of servers currently listening to music
 | 
			
		||||
            yield return ("%music.servers%", () =>
 | 
			
		||||
            {
 | 
			
		||||
                var count = _players
 | 
			
		||||
                    .Select(x => x.Value.GetCurrentTrack(out _))
 | 
			
		||||
                    .Count(x => !(x is null));
 | 
			
		||||
 | 
			
		||||
                return count.ToString();
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            yield return ("%music.queued%", () =>
 | 
			
		||||
            {
 | 
			
		||||
                var count = _players
 | 
			
		||||
                    .Sum(x => x.Value.GetQueuedTracks().Count);
 | 
			
		||||
 | 
			
		||||
                return count.ToString();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #region Settings
 | 
			
		||||
 | 
			
		||||
        private async Task<MusicPlayerSettings> GetSettingsInternalAsync(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            if (_settings.TryGetValue(guildId, out var settings))
 | 
			
		||||
                return settings;
 | 
			
		||||
            
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var toReturn = _settings[guildId] = await uow._context.MusicPlayerSettings.ForGuildAsync(guildId);
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
            return toReturn;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private async Task ModifySettingsInternalAsync<TState>(
 | 
			
		||||
            ulong guildId,
 | 
			
		||||
            Action<MusicPlayerSettings, TState> action,
 | 
			
		||||
            TState state)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var ms = await uow._context.MusicPlayerSettings.ForGuildAsync(guildId);
 | 
			
		||||
            action(ms, state);
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            _settings[guildId] = ms;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public async Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId)
 | 
			
		||||
        {
 | 
			
		||||
            if (channelId is null)
 | 
			
		||||
            {
 | 
			
		||||
                await UnsetMusicChannelAsync(guildId);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var channel = _client.GetGuild(guildId)?.GetTextChannel(channelId.Value);
 | 
			
		||||
            if (channel is null)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            await ModifySettingsInternalAsync(guildId, (settings, chId) =>
 | 
			
		||||
            {
 | 
			
		||||
                settings.MusicChannelId = chId;
 | 
			
		||||
            }, channelId);
 | 
			
		||||
 | 
			
		||||
            _outputChannels.AddOrUpdate(guildId,
 | 
			
		||||
                (channel, channel),
 | 
			
		||||
                (key, old) => (old.Default, channel));
 | 
			
		||||
            
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task UnsetMusicChannelAsync(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            await ModifySettingsInternalAsync(guildId, (settings, _) =>
 | 
			
		||||
            {
 | 
			
		||||
                settings.MusicChannelId = null;
 | 
			
		||||
            }, (ulong?)null);
 | 
			
		||||
 | 
			
		||||
            if (_outputChannels.TryGetValue(guildId, out var old))
 | 
			
		||||
                _outputChannels[guildId] = (old.Default, null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType)
 | 
			
		||||
        {
 | 
			
		||||
            await ModifySettingsInternalAsync(guildId, (settings, type) =>
 | 
			
		||||
            {
 | 
			
		||||
                settings.PlayerRepeat = type;
 | 
			
		||||
            }, repeatType);
 | 
			
		||||
 | 
			
		||||
            if (TryGetMusicPlayer(guildId, out var mp))
 | 
			
		||||
                mp.SetRepeat(repeatType);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task SetVolumeAsync(ulong guildId, int value)
 | 
			
		||||
        {
 | 
			
		||||
            if (value < 0 || value > 100)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(value));
 | 
			
		||||
            
 | 
			
		||||
            await ModifySettingsInternalAsync(guildId, (settings, newValue) =>
 | 
			
		||||
            {
 | 
			
		||||
                settings.Volume = newValue;
 | 
			
		||||
            }, value);
 | 
			
		||||
            
 | 
			
		||||
            if (TryGetMusicPlayer(guildId, out var mp))
 | 
			
		||||
                mp.SetVolume(value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> ToggleAutoDisconnectAsync(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            var newState = false;
 | 
			
		||||
            await ModifySettingsInternalAsync(guildId, (settings, _) =>
 | 
			
		||||
            {
 | 
			
		||||
                newState = settings.AutoDisconnect = !settings.AutoDisconnect;
 | 
			
		||||
            }, default(object));
 | 
			
		||||
 | 
			
		||||
            return newState;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<QualityPreset> GetMusicQualityAsync(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var settings = await uow._context.MusicPlayerSettings.ForGuildAsync(guildId);
 | 
			
		||||
            return settings.QualityPreset;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset)
 | 
			
		||||
        {
 | 
			
		||||
            return ModifySettingsInternalAsync(guildId, (settings, _) =>
 | 
			
		||||
            {
 | 
			
		||||
                settings.QualityPreset = preset;
 | 
			
		||||
            }, preset);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										297
									
								
								src/NadekoBot/Modules/Music/Services/_OldMusicService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								src/NadekoBot/Modules/Music/Services/_OldMusicService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,297 @@
 | 
			
		||||
// using System.Collections.Concurrent;
 | 
			
		||||
// using System.Linq;
 | 
			
		||||
// using System.Threading.Tasks;
 | 
			
		||||
// using Discord;
 | 
			
		||||
// using NadekoBot.Extensions;
 | 
			
		||||
// using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
// using System.IO;
 | 
			
		||||
// using Discord.Commands;
 | 
			
		||||
// using Discord.WebSocket;
 | 
			
		||||
// using NadekoBot.Common;
 | 
			
		||||
// using NadekoBot.Core.Services.Impl;
 | 
			
		||||
// using NadekoBot.Core.Services;
 | 
			
		||||
// using NadekoBot.Modules.Music.Common;
 | 
			
		||||
// using NadekoBot.Modules.Music.Common.Exceptions;
 | 
			
		||||
// using NadekoBot.Modules.Music.Common.SongResolver;
 | 
			
		||||
// using NadekoBot.Common.Collections;
 | 
			
		||||
// using Microsoft.EntityFrameworkCore;
 | 
			
		||||
// using Serilog;
 | 
			
		||||
//
 | 
			
		||||
// namespace NadekoBot.Modules.Music.Services
 | 
			
		||||
// {
 | 
			
		||||
//     public class MusicService : INService, IUnloadableService
 | 
			
		||||
//     {
 | 
			
		||||
//         public const string MusicDataPath = "data/musicdata";
 | 
			
		||||
//
 | 
			
		||||
//         private readonly IGoogleApiService _google;
 | 
			
		||||
//         private readonly IBotStrings _strings;
 | 
			
		||||
//         private readonly DbService _db;
 | 
			
		||||
//         private readonly AyuVoiceStateService _tracker;
 | 
			
		||||
//         private readonly ConcurrentDictionary<ulong, MusicSettings> _musicSettings;
 | 
			
		||||
//         private readonly SoundCloudApiService _sc;
 | 
			
		||||
//         private readonly IBotCredentials _creds;
 | 
			
		||||
//         private readonly ConcurrentDictionary<ulong, float> _defaultVolumes;
 | 
			
		||||
//
 | 
			
		||||
//         public ConcurrentHashSet<ulong> AutoDcServers { get; }
 | 
			
		||||
//
 | 
			
		||||
//         private readonly DiscordSocketClient _client;
 | 
			
		||||
//
 | 
			
		||||
//         public ConcurrentDictionary<ulong, MusicPlayer> MusicPlayers { get; } = new ConcurrentDictionary<ulong, MusicPlayer>();
 | 
			
		||||
//
 | 
			
		||||
//         public MusicService(DiscordSocketClient client, IGoogleApiService google,
 | 
			
		||||
//             IBotStrings strings, DbService db, AyuVoiceStateService tracker,
 | 
			
		||||
//             SoundCloudApiService sc, IBotCredentials creds, NadekoBot bot)
 | 
			
		||||
//         {
 | 
			
		||||
//             _client = client;
 | 
			
		||||
//             _google = google;
 | 
			
		||||
//             _strings = strings;
 | 
			
		||||
//             _db = db;
 | 
			
		||||
//             _tracker = tracker;
 | 
			
		||||
//             _sc = sc;
 | 
			
		||||
//             _creds = creds;
 | 
			
		||||
//             _musicSettings = bot.AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.MusicSettings)
 | 
			
		||||
//                 .ToConcurrent();
 | 
			
		||||
//
 | 
			
		||||
//             _client.LeftGuild += _client_LeftGuild;
 | 
			
		||||
//             try { Directory.Delete(MusicDataPath, true); } catch { }
 | 
			
		||||
//
 | 
			
		||||
//             _defaultVolumes = new ConcurrentDictionary<ulong, float>(
 | 
			
		||||
//                 bot.AllGuildConfigs
 | 
			
		||||
//                     .ToDictionary(x => x.GuildId, x => x.DefaultMusicVolume));
 | 
			
		||||
//
 | 
			
		||||
//             AutoDcServers = new ConcurrentHashSet<ulong>(bot.AllGuildConfigs.Where(x => x.AutoDcFromVc).Select(x => x.GuildId));
 | 
			
		||||
//
 | 
			
		||||
//             Directory.CreateDirectory(MusicDataPath);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public Task Unload()
 | 
			
		||||
//         {
 | 
			
		||||
//             _client.LeftGuild -= _client_LeftGuild;
 | 
			
		||||
//             return Task.CompletedTask;
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         private Task _client_LeftGuild(SocketGuild arg)
 | 
			
		||||
//         {
 | 
			
		||||
//             var t = DestroyPlayer(arg.Id);
 | 
			
		||||
//             return Task.CompletedTask;
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public float GetDefaultVolume(ulong guildId)
 | 
			
		||||
//         {
 | 
			
		||||
//             return _defaultVolumes.GetOrAdd(guildId, (id) =>
 | 
			
		||||
//             {
 | 
			
		||||
//                 using (var uow = _db.GetDbContext())
 | 
			
		||||
//                 {
 | 
			
		||||
//                     return uow.GuildConfigs.ForId(guildId, set => set).DefaultMusicVolume;
 | 
			
		||||
//                 }
 | 
			
		||||
//             });
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public Task<MusicPlayer> GetOrCreatePlayer(ICommandContext context)
 | 
			
		||||
//         {
 | 
			
		||||
//             var gUsr = (IGuildUser)context.User;
 | 
			
		||||
//             var txtCh = (ITextChannel)context.Channel;
 | 
			
		||||
//             var vCh = gUsr.VoiceChannel;
 | 
			
		||||
//             return GetOrCreatePlayer(context.Guild.Id, vCh, txtCh);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public async Task<MusicPlayer> GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh)
 | 
			
		||||
//         {
 | 
			
		||||
//             string GetText(string text, params object[] replacements) =>
 | 
			
		||||
//                 _strings.GetText(text, textCh.Guild.Id, replacements);
 | 
			
		||||
//             
 | 
			
		||||
//             if (voiceCh == null || voiceCh.Guild != textCh.Guild)
 | 
			
		||||
//             {
 | 
			
		||||
//                 if (textCh != null)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false);
 | 
			
		||||
//                 }
 | 
			
		||||
//                 throw new NotInVoiceChannelException();
 | 
			
		||||
//             }
 | 
			
		||||
//             return MusicPlayers.GetOrAdd(guildId, _ =>
 | 
			
		||||
//             {
 | 
			
		||||
//                 var vol = GetDefaultVolume(guildId);
 | 
			
		||||
//                 if (!_musicSettings.TryGetValue(guildId, out var ms))
 | 
			
		||||
//                     ms = new MusicSettings();
 | 
			
		||||
//
 | 
			
		||||
//                 var mp = new MusicPlayer(this, ms, _google, voiceCh, textCh, vol, _tracker);
 | 
			
		||||
//
 | 
			
		||||
//                 IUserMessage playingMessage = null;
 | 
			
		||||
//                 IUserMessage lastFinishedMessage = null;
 | 
			
		||||
//
 | 
			
		||||
//                 mp.OnCompleted += async (s, song) =>
 | 
			
		||||
//                 {
 | 
			
		||||
//                     try
 | 
			
		||||
//                     {
 | 
			
		||||
//                         lastFinishedMessage?.DeleteAfter(0);
 | 
			
		||||
//
 | 
			
		||||
//                         try
 | 
			
		||||
//                         {
 | 
			
		||||
//                             lastFinishedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
//                                     .WithAuthor(eab => eab.WithName(GetText("finished_song")).WithMusicIcon())
 | 
			
		||||
//                                     .WithDescription(song.PrettyName)
 | 
			
		||||
//                                     .WithFooter(ef => ef.WithText(song.PrettyInfo)))
 | 
			
		||||
//                                 .ConfigureAwait(false);
 | 
			
		||||
//                         }
 | 
			
		||||
//                         catch
 | 
			
		||||
//                         {
 | 
			
		||||
//                             // ignored
 | 
			
		||||
//                         }
 | 
			
		||||
//
 | 
			
		||||
//                         var (Index, Current) = mp.Current;
 | 
			
		||||
//                         if (Current == null
 | 
			
		||||
//                             && !mp.RepeatCurrentSong
 | 
			
		||||
//                             && !mp.RepeatPlaylist
 | 
			
		||||
//                             && !mp.FairPlay
 | 
			
		||||
//                             && AutoDcServers.Contains(guildId))
 | 
			
		||||
//                         {
 | 
			
		||||
//                             await DestroyPlayer(guildId).ConfigureAwait(false);
 | 
			
		||||
//                         }
 | 
			
		||||
//                     }
 | 
			
		||||
//                     catch
 | 
			
		||||
//                     {
 | 
			
		||||
//                         // ignored
 | 
			
		||||
//                     }
 | 
			
		||||
//                 };
 | 
			
		||||
//                 mp.OnStarted += async (player, song) =>
 | 
			
		||||
//                 {
 | 
			
		||||
//                     //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); }
 | 
			
		||||
//                     //catch
 | 
			
		||||
//                     //{
 | 
			
		||||
//                     //    // ignored
 | 
			
		||||
//                     //}
 | 
			
		||||
//                     var sender = player;
 | 
			
		||||
//                     if (sender == null)
 | 
			
		||||
//                         return;
 | 
			
		||||
//                     try
 | 
			
		||||
//                     {
 | 
			
		||||
//                         playingMessage?.DeleteAfter(0);
 | 
			
		||||
//
 | 
			
		||||
//                         playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
//                                                     .WithAuthor(eab => eab.WithName(GetText("playing_song", song.Index + 1)).WithMusicIcon())
 | 
			
		||||
//                                                     .WithDescription(song.Song.PrettyName)
 | 
			
		||||
//                                                     .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.Song.PrettyInfo)))
 | 
			
		||||
//                                                     .ConfigureAwait(false);
 | 
			
		||||
//                     }
 | 
			
		||||
//                     catch
 | 
			
		||||
//                     {
 | 
			
		||||
//                         // ignored
 | 
			
		||||
//                     }
 | 
			
		||||
//                 };
 | 
			
		||||
//                 mp.OnPauseChanged += async (player, paused) =>
 | 
			
		||||
//                 {
 | 
			
		||||
//                     try
 | 
			
		||||
//                     {
 | 
			
		||||
//                         IUserMessage msg;
 | 
			
		||||
//                         if (paused)
 | 
			
		||||
//                             msg = await mp.OutputTextChannel.SendConfirmAsync(GetText("paused")).ConfigureAwait(false);
 | 
			
		||||
//                         else
 | 
			
		||||
//                             msg = await mp.OutputTextChannel.SendConfirmAsync(GetText("resumed")).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//                         msg?.DeleteAfter(10);
 | 
			
		||||
//                     }
 | 
			
		||||
//                     catch
 | 
			
		||||
//                     {
 | 
			
		||||
//                         // ignored
 | 
			
		||||
//                     }
 | 
			
		||||
//                 };
 | 
			
		||||
//                 Log.Information("Done creating");
 | 
			
		||||
//                 return mp;
 | 
			
		||||
//             });
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public MusicPlayer GetPlayerOrDefault(ulong guildId)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (MusicPlayers.TryGetValue(guildId, out var mp))
 | 
			
		||||
//                 return mp;
 | 
			
		||||
//             else
 | 
			
		||||
//                 return null;
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public async Task TryQueueRelatedSongAsync(SongInfo song, ITextChannel txtCh, IVoiceChannel vch)
 | 
			
		||||
//         {
 | 
			
		||||
//             var related = (await _google.GetRelatedVideosAsync(song.VideoId, 4).ConfigureAwait(false)).ToArray();
 | 
			
		||||
//             if (!related.Any())
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.YouTube).ConfigureAwait(false);
 | 
			
		||||
//             if (si == null)
 | 
			
		||||
//                 throw new SongNotFoundException();
 | 
			
		||||
//             var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh).ConfigureAwait(false);
 | 
			
		||||
//             mp.Enqueue(si);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public async Task<SongInfo> ResolveSong(string query, string queuerName, MusicType? musicType = null)
 | 
			
		||||
//         {
 | 
			
		||||
//             query.ThrowIfNull(nameof(query));
 | 
			
		||||
//
 | 
			
		||||
//             ISongResolverFactory resolverFactory = new SongResolverFactory(_sc);
 | 
			
		||||
//             var strategy = await resolverFactory.GetResolveStrategy(query, musicType).ConfigureAwait(false);
 | 
			
		||||
//             var sinfo = await strategy.ResolveSong(query).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             if (sinfo == null)
 | 
			
		||||
//                 return null;
 | 
			
		||||
//
 | 
			
		||||
//             sinfo.QueuerName = queuerName;
 | 
			
		||||
//
 | 
			
		||||
//             return sinfo;
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public async Task DestroyAllPlayers()
 | 
			
		||||
//         {
 | 
			
		||||
//             foreach (var key in MusicPlayers.Keys)
 | 
			
		||||
//             {
 | 
			
		||||
//                 await DestroyPlayer(key).ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public async Task DestroyPlayer(ulong id)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (MusicPlayers.TryRemove(id, out var mp))
 | 
			
		||||
//                 await mp.Destroy().ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public bool ToggleAutoDc(ulong id)
 | 
			
		||||
//         {
 | 
			
		||||
//             bool val;
 | 
			
		||||
//             using (var uow = _db.GetDbContext())
 | 
			
		||||
//             {
 | 
			
		||||
//                 var gc = uow.GuildConfigs.ForId(id, set => set);
 | 
			
		||||
//                 val = gc.AutoDcFromVc = !gc.AutoDcFromVc;
 | 
			
		||||
//                 uow.SaveChanges();
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             if (val)
 | 
			
		||||
//                 AutoDcServers.Add(id);
 | 
			
		||||
//             else
 | 
			
		||||
//                 AutoDcServers.TryRemove(id);
 | 
			
		||||
//
 | 
			
		||||
//             return val;
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public void UpdateSettings(ulong id, MusicSettings musicSettings)
 | 
			
		||||
//         {
 | 
			
		||||
//             _musicSettings.AddOrUpdate(id, musicSettings, delegate { return musicSettings; });
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public void SetMusicChannel(ulong guildId, ulong? cid)
 | 
			
		||||
//         {
 | 
			
		||||
//             using (var uow = _db.GetDbContext())
 | 
			
		||||
//             {
 | 
			
		||||
//                 var ms = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.MusicSettings)).MusicSettings;
 | 
			
		||||
//                 ms.MusicChannelId = cid;
 | 
			
		||||
//                 uow.SaveChanges();
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public void SetSongAutoDelete(ulong guildId, bool val)
 | 
			
		||||
//         {
 | 
			
		||||
//             using (var uow = _db.GetDbContext())
 | 
			
		||||
//             {
 | 
			
		||||
//                 var ms = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.MusicSettings)).MusicSettings;
 | 
			
		||||
//                 ms.SongAutoDelete = val;
 | 
			
		||||
//                 uow.SaveChanges();
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
							
								
								
									
										75
									
								
								src/NadekoBot/Modules/Music/Services/extractor/Misc.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/NadekoBot/Modules/Music/Services/extractor/Misc.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Services
 | 
			
		||||
{
 | 
			
		||||
    
 | 
			
		||||
    public sealed partial class YtLoader
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        public class InitRange
 | 
			
		||||
        {
 | 
			
		||||
            public string Start { get; set; }
 | 
			
		||||
            public string End { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class IndexRange
 | 
			
		||||
        {
 | 
			
		||||
            public string Start { get; set; }
 | 
			
		||||
            public string End { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class ColorInfo
 | 
			
		||||
        {
 | 
			
		||||
            public string Primaries { get; set; }
 | 
			
		||||
            public string TransferCharacteristics { get; set; }
 | 
			
		||||
            public string MatrixCoefficients { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class YtAdaptiveFormat
 | 
			
		||||
        {
 | 
			
		||||
            public int Itag { get; set; }
 | 
			
		||||
            public string MimeType { get; set; }
 | 
			
		||||
            public int Bitrate { get; set; }
 | 
			
		||||
            public int Width { get; set; }
 | 
			
		||||
            public int Height { get; set; }
 | 
			
		||||
            public InitRange InitRange { get; set; }
 | 
			
		||||
            public IndexRange IndexRange { get; set; }
 | 
			
		||||
            public string LastModified { get; set; }
 | 
			
		||||
            public string ContentLength { get; set; }
 | 
			
		||||
            public string Quality { get; set; }
 | 
			
		||||
            public int Fps { get; set; }
 | 
			
		||||
            public string QualityLabel { get; set; }
 | 
			
		||||
            public string ProjectionType { get; set; }
 | 
			
		||||
            public int AverageBitrate { get; set; }
 | 
			
		||||
            public ColorInfo ColorInfo { get; set; }
 | 
			
		||||
            public string ApproxDurationMs { get; set; }
 | 
			
		||||
            public string SignatureCipher { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public abstract class TrackInfo
 | 
			
		||||
        {
 | 
			
		||||
            public abstract string Url { get; }
 | 
			
		||||
            public abstract string Title { get; }
 | 
			
		||||
            public abstract TimeSpan Duration { get; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public sealed class YtTrackInfo : TrackInfo
 | 
			
		||||
        {
 | 
			
		||||
            private const string BaseYoutubeUrl = "https://youtube.com/watch?v=";
 | 
			
		||||
            public override string Url { get; }
 | 
			
		||||
            public override string Title { get; }
 | 
			
		||||
            public override TimeSpan Duration { get; }
 | 
			
		||||
 | 
			
		||||
            private readonly string _videoId;
 | 
			
		||||
 | 
			
		||||
            public YtTrackInfo(string title, string videoId, TimeSpan duration)
 | 
			
		||||
            {
 | 
			
		||||
                Title = title;
 | 
			
		||||
                Url = BaseYoutubeUrl + videoId;
 | 
			
		||||
                Duration = duration;
 | 
			
		||||
 | 
			
		||||
                _videoId = videoId;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord.Net;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Services
 | 
			
		||||
{
 | 
			
		||||
    public sealed partial class YtLoader
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
        private static readonly byte[] YT_RESULT_INITIAL_DATA = Encoding.UTF8.GetBytes("var ytInitialData = ");
 | 
			
		||||
        private static readonly byte[] YT_RESULT_JSON_END = Encoding.UTF8.GetBytes(";<");
 | 
			
		||||
        
 | 
			
		||||
        private static readonly string[] durationFormats = new[]
 | 
			
		||||
        {
 | 
			
		||||
            @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss", @"hhh\:mm\:ss"
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        public YtLoader(IHttpClientFactory httpFactory)
 | 
			
		||||
        {
 | 
			
		||||
            _httpFactory = httpFactory;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // public async Task<TrackInfo> LoadTrackByIdAsync(string videoId)
 | 
			
		||||
        // {
 | 
			
		||||
        //     using var http = new HttpClient();
 | 
			
		||||
        //     http.DefaultRequestHeaders.Add("X-YouTube-Client-Name", "1");
 | 
			
		||||
        //     http.DefaultRequestHeaders.Add("X-YouTube-Client-Version", "2.20210520.09.00");
 | 
			
		||||
        //     http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
 | 
			
		||||
        //
 | 
			
		||||
        //     var responseString = await http.GetStringAsync($"https://youtube.com?" +
 | 
			
		||||
        //                         $"pbj=1" +
 | 
			
		||||
        //                         $"&hl=en" +
 | 
			
		||||
        //                         $"&v=" + videoId);
 | 
			
		||||
        //
 | 
			
		||||
        //     var jsonDoc = JsonDocument.Parse(responseString).RootElement;
 | 
			
		||||
        //     var elem = jsonDoc.EnumerateArray()
 | 
			
		||||
        //         .FirstOrDefault(x => x.TryGetProperty("page", out var elem) && elem.GetString() == "watch");
 | 
			
		||||
        //
 | 
			
		||||
        //     var formatsJsonArray = elem.GetProperty("streamingdata")
 | 
			
		||||
        //         .GetProperty("formats")
 | 
			
		||||
        //         .GetRawText();
 | 
			
		||||
        //     
 | 
			
		||||
        //     var formats = JsonSerializer.Deserialize<List<YtAdaptiveFormat>>(formatsJsonArray);
 | 
			
		||||
        //     var result = formats
 | 
			
		||||
        //         .Where(x => x.MimeType.StartsWith("audio/"))
 | 
			
		||||
        //         .OrderByDescending(x => x.Bitrate)
 | 
			
		||||
        //         .FirstOrDefault();
 | 
			
		||||
        //
 | 
			
		||||
        //     if (result is null)
 | 
			
		||||
        //         return null;
 | 
			
		||||
        //
 | 
			
		||||
        //     return new YtTrackInfo("1", "2", TimeSpan.Zero);
 | 
			
		||||
        // }
 | 
			
		||||
 | 
			
		||||
        public async Task<IList<TrackInfo>> LoadResultsAsync(string query)
 | 
			
		||||
        {
 | 
			
		||||
            query = Uri.EscapeDataString(query);
 | 
			
		||||
            
 | 
			
		||||
            using var http = _httpFactory.CreateClient();
 | 
			
		||||
            http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
 | 
			
		||||
 | 
			
		||||
            byte[] response;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                response = await http.GetByteArrayAsync($"https://youtube.com/results?hl=en&search_query={query}");
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpRequestException ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Unable to retrieve data with YtLoader: {ErrorMessage}", ex.Message);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // there is a lot of useless html above the script tag, however if html gets significantly reduced
 | 
			
		||||
            // this will result in the json being cut off
 | 
			
		||||
 | 
			
		||||
            var mem = GetScriptResponseSpan(response);
 | 
			
		||||
            var root = JsonDocument.Parse(mem).RootElement;
 | 
			
		||||
 | 
			
		||||
            var tracksJsonItems = root
 | 
			
		||||
                .GetProperty("contents")
 | 
			
		||||
                .GetProperty("twoColumnSearchResultsRenderer")
 | 
			
		||||
                .GetProperty("primaryContents")
 | 
			
		||||
                .GetProperty("sectionListRenderer")
 | 
			
		||||
                .GetProperty("contents")
 | 
			
		||||
                [0]
 | 
			
		||||
                .GetProperty("itemSectionRenderer")
 | 
			
		||||
                .GetProperty("contents")
 | 
			
		||||
                .EnumerateArray();
 | 
			
		||||
            
 | 
			
		||||
            var tracks = new List<TrackInfo>();
 | 
			
		||||
            foreach (var track in tracksJsonItems)
 | 
			
		||||
            {
 | 
			
		||||
                if(!track.TryGetProperty("videoRenderer", out var elem))
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                var videoId = elem.GetProperty("videoId").GetString();
 | 
			
		||||
                // var thumb = elem.GetProperty("thumbnail").GetProperty("thumbnails")[0].GetProperty("url").GetString();
 | 
			
		||||
                var title = elem.GetProperty("title").GetProperty("runs")[0].GetProperty("text").GetString();
 | 
			
		||||
                var durationString = elem.GetProperty("lengthText").GetProperty("simpleText").GetString();
 | 
			
		||||
 | 
			
		||||
                if (!TimeSpan.TryParseExact(durationString, durationFormats, CultureInfo.InvariantCulture,
 | 
			
		||||
                    out var duration))
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning("Cannot parse duration: {DurationString}", durationString);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                tracks.Add(new YtTrackInfo(title, videoId, duration));
 | 
			
		||||
                if (tracks.Count >= 5)
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            return tracks;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Memory<byte> GetScriptResponseSpan(byte[] response)
 | 
			
		||||
        {
 | 
			
		||||
            var responseSpan = response.AsSpan().Slice(140_000);
 | 
			
		||||
            var startIndex = responseSpan.IndexOf(YT_RESULT_INITIAL_DATA);
 | 
			
		||||
            if (startIndex == -1)
 | 
			
		||||
                return null; // todo try selecting html
 | 
			
		||||
            startIndex += YT_RESULT_INITIAL_DATA.Length;
 | 
			
		||||
 | 
			
		||||
            var endIndex = 140_000 + startIndex + responseSpan.Slice(startIndex + 20_000).IndexOf(YT_RESULT_JSON_END) + 20_000;
 | 
			
		||||
            startIndex += 140_000;
 | 
			
		||||
            return response.AsMemory(
 | 
			
		||||
                startIndex,
 | 
			
		||||
                endIndex - startIndex
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										984
									
								
								src/NadekoBot/Modules/Music/_OldMusic.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										984
									
								
								src/NadekoBot/Modules/Music/_OldMusic.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,984 @@
 | 
			
		||||
// using Discord;
 | 
			
		||||
// using Discord.Commands;
 | 
			
		||||
// using Discord.WebSocket;
 | 
			
		||||
// using NadekoBot.Common;
 | 
			
		||||
// using NadekoBot.Common.Attributes;
 | 
			
		||||
// using NadekoBot.Common.Collections;
 | 
			
		||||
// using NadekoBot.Core.Services;
 | 
			
		||||
// using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
// using NadekoBot.Core.Services.Impl;
 | 
			
		||||
// using NadekoBot.Extensions;
 | 
			
		||||
// using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
// using NadekoBot.Modules.Music.Common;
 | 
			
		||||
// using NadekoBot.Modules.Music.Common.Exceptions;
 | 
			
		||||
// using NadekoBot.Modules.Music.Extensions;
 | 
			
		||||
// using NadekoBot.Modules.Music.Services;
 | 
			
		||||
// using Newtonsoft.Json.Linq;
 | 
			
		||||
// using System;
 | 
			
		||||
// using System.Collections.Generic;
 | 
			
		||||
// using System.IO;
 | 
			
		||||
// using System.Linq;
 | 
			
		||||
// using System.Net.Http;
 | 
			
		||||
// using System.Threading.Tasks;
 | 
			
		||||
// using Serilog;
 | 
			
		||||
//
 | 
			
		||||
// namespace NadekoBot.Modules.Music
 | 
			
		||||
// {
 | 
			
		||||
//     [NoPublicBot]
 | 
			
		||||
//     public class Music : NadekoModule<MusicService>
 | 
			
		||||
//     {
 | 
			
		||||
//         private readonly DiscordSocketClient _client;
 | 
			
		||||
//         private readonly IBotCredentials _creds;
 | 
			
		||||
//         private readonly IGoogleApiService _google;
 | 
			
		||||
//         private readonly DbService _db;
 | 
			
		||||
//         private readonly LogCommandService _logService;
 | 
			
		||||
//
 | 
			
		||||
//         public Music(DiscordSocketClient client,
 | 
			
		||||
//             IBotCredentials creds,
 | 
			
		||||
//             IGoogleApiService google,
 | 
			
		||||
//             DbService db,
 | 
			
		||||
//             LogCommandService logService)
 | 
			
		||||
//         {
 | 
			
		||||
//             _client = client;
 | 
			
		||||
//             _creds = creds;
 | 
			
		||||
//             _google = google;
 | 
			
		||||
//             _db = db;
 | 
			
		||||
//             _logService = logService;
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         //private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState)
 | 
			
		||||
//         //{
 | 
			
		||||
//         //    var t = Task.Run(() =>
 | 
			
		||||
//         //    {
 | 
			
		||||
//         //        var usr = iusr as SocketGuildUser;
 | 
			
		||||
//         //        if (usr == null ||
 | 
			
		||||
//         //            oldState.VoiceChannel == newState.VoiceChannel)
 | 
			
		||||
//         //            return;
 | 
			
		||||
//
 | 
			
		||||
//         //        var player = _music.GetPlayerOrDefault(usr.Guild.Id);
 | 
			
		||||
//
 | 
			
		||||
//         //        if (player == null)
 | 
			
		||||
//         //            return;
 | 
			
		||||
//
 | 
			
		||||
//         //        try
 | 
			
		||||
//         //        {
 | 
			
		||||
//         //            //if bot moved
 | 
			
		||||
//         //            if ((player.VoiceChannel == oldState.VoiceChannel) &&
 | 
			
		||||
//         //                    usr.Id == _client.CurrentUser.Id)
 | 
			
		||||
//         //            {
 | 
			
		||||
//         //                //if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel
 | 
			
		||||
//         //                //    player.TogglePause();
 | 
			
		||||
//         //                //else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel
 | 
			
		||||
//         //                //    player.TogglePause();
 | 
			
		||||
//
 | 
			
		||||
//         //               // player.SetVoiceChannel(newState.VoiceChannel);
 | 
			
		||||
//         //                return;
 | 
			
		||||
//         //            }
 | 
			
		||||
//
 | 
			
		||||
//         //            ////if some other user moved
 | 
			
		||||
//         //            //if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause
 | 
			
		||||
//         //            //        player.Paused &&
 | 
			
		||||
//         //            //        newState.VoiceChannel.Users.Count >= 2) ||  // keep in mind bot is in the channel (+1)
 | 
			
		||||
//         //            //    (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause
 | 
			
		||||
//         //            //        !player.Paused &&
 | 
			
		||||
//         //            //        oldState.VoiceChannel.Users.Count == 1))
 | 
			
		||||
//         //            //{
 | 
			
		||||
//         //            //    player.TogglePause();
 | 
			
		||||
//         //            //    return;
 | 
			
		||||
//         //            //}
 | 
			
		||||
//         //        }
 | 
			
		||||
//         //        catch
 | 
			
		||||
//         //        {
 | 
			
		||||
//         //            // ignored
 | 
			
		||||
//         //        }
 | 
			
		||||
//         //    });
 | 
			
		||||
//         //    return Task.CompletedTask;
 | 
			
		||||
//         //}
 | 
			
		||||
//
 | 
			
		||||
//         private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent, bool queueFirst = false, bool forcePlay = false)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (songInfo == null)
 | 
			
		||||
//             {
 | 
			
		||||
//                 if (!silent)
 | 
			
		||||
//                     await ReplyErrorLocalizedAsync("song_not_found").ConfigureAwait(false);
 | 
			
		||||
//                 return;
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             int index;
 | 
			
		||||
//             try
 | 
			
		||||
//             {
 | 
			
		||||
//                 index = queueFirst
 | 
			
		||||
//                     ? mp.EnqueueNext(songInfo, forcePlay)
 | 
			
		||||
//                     : mp.Enqueue(songInfo, forcePlay);
 | 
			
		||||
//             }
 | 
			
		||||
//             catch (QueueFullException)
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyErrorLocalizedAsync("queue_full", mp.MaxQueueSize).ConfigureAwait(false);
 | 
			
		||||
//                 throw;
 | 
			
		||||
//             }
 | 
			
		||||
//             if (index != -1)
 | 
			
		||||
//             {
 | 
			
		||||
//                 if (!silent)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     try
 | 
			
		||||
//                     {
 | 
			
		||||
//                         var embed = new EmbedBuilder().WithOkColor()
 | 
			
		||||
//                                         .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index + 1)).WithMusicIcon())
 | 
			
		||||
//                                         .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ")
 | 
			
		||||
//                                         .WithFooter(ef => ef.WithText(songInfo.PrettyProvider));
 | 
			
		||||
//
 | 
			
		||||
//                         if (Uri.IsWellFormedUriString(songInfo.Thumbnail, UriKind.Absolute))
 | 
			
		||||
//                             embed.WithThumbnailUrl(songInfo.Thumbnail);
 | 
			
		||||
//
 | 
			
		||||
//                         var queuedMessage = await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
//                         if (mp.Stopped)
 | 
			
		||||
//                         {
 | 
			
		||||
//                             (await ReplyErrorLocalizedAsync("queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10, _logService);
 | 
			
		||||
//                         }
 | 
			
		||||
//                         queuedMessage?.DeleteAfter(10, _logService);
 | 
			
		||||
//                     }
 | 
			
		||||
//                     catch
 | 
			
		||||
//                     {
 | 
			
		||||
//                         // ignored
 | 
			
		||||
//                     }
 | 
			
		||||
//                 }
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Play([Leftover] string query = null)
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             if (string.IsNullOrWhiteSpace(query))
 | 
			
		||||
//             {
 | 
			
		||||
//                 await Next().ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//             else if (int.TryParse(query, out var index))
 | 
			
		||||
//                 if (index >= 1)
 | 
			
		||||
//                     mp.SetIndex(index - 1);
 | 
			
		||||
//                 else
 | 
			
		||||
//                     return;
 | 
			
		||||
//             else
 | 
			
		||||
//             {
 | 
			
		||||
//                 try
 | 
			
		||||
//                 {
 | 
			
		||||
//                     await InternalPlay(query, forceplay: true).ConfigureAwait(false);
 | 
			
		||||
//                 }
 | 
			
		||||
//                 catch { }
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public Task Queue([Leftover] string query)
 | 
			
		||||
//             => InternalPlay(query, forceplay: false);
 | 
			
		||||
//
 | 
			
		||||
//         private async Task InternalPlay(string query, bool forceplay)
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var songInfo = await _service.ResolveSong(query, ctx.User.ToString()).ConfigureAwait(false);
 | 
			
		||||
//             try { await InternalQueue(mp, songInfo, false, forcePlay: forceplay).ConfigureAwait(false); } catch (QueueFullException) { return; }
 | 
			
		||||
//             if ((await ctx.Guild.GetCurrentUserAsync().ConfigureAwait(false)).GetPermissions((IGuildChannel)ctx.Channel).ManageMessages)
 | 
			
		||||
//             {
 | 
			
		||||
//                 ctx.Message.DeleteAfter(10, _logService);
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task QueueNext([Leftover] string query)
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var songInfo = await _service.ResolveSong(query, ctx.User.ToString()).ConfigureAwait(false);
 | 
			
		||||
//             try { await InternalQueue(mp, songInfo, false, true).ConfigureAwait(false); } catch (QueueFullException) { return; }
 | 
			
		||||
//             if ((await ctx.Guild.GetCurrentUserAsync().ConfigureAwait(false)).GetPermissions((IGuildChannel)ctx.Channel).ManageMessages)
 | 
			
		||||
//             {
 | 
			
		||||
//                 ctx.Message.DeleteAfter(10, _logService);
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task QueueSearch([Leftover] string query)
 | 
			
		||||
//         {
 | 
			
		||||
//             var videos = (await _google.GetVideoInfosByKeywordAsync(query, 5).ConfigureAwait(false))
 | 
			
		||||
//                 .ToArray();
 | 
			
		||||
//
 | 
			
		||||
//             if (!videos.Any())
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyErrorLocalizedAsync("song_not_found").ConfigureAwait(false);
 | 
			
		||||
//                 return;
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             var msg = await ctx.Channel.SendConfirmAsync(string.Join("\n", videos.Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Name)}\n\t{x.Url}"))).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             try
 | 
			
		||||
//             {
 | 
			
		||||
//                 var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id).ConfigureAwait(false);
 | 
			
		||||
//                 if (input == null
 | 
			
		||||
//                     || !int.TryParse(input, out var index)
 | 
			
		||||
//                     || (index -= 1) < 0
 | 
			
		||||
//                     || index >= videos.Length)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     _logService.AddDeleteIgnore(msg.Id);
 | 
			
		||||
//                     try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
 | 
			
		||||
//                     return;
 | 
			
		||||
//                 }
 | 
			
		||||
//
 | 
			
		||||
//                 query = videos[index].Url;
 | 
			
		||||
//
 | 
			
		||||
//                 await Queue(query).ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//             finally
 | 
			
		||||
//             {
 | 
			
		||||
//                 _logService.AddDeleteIgnore(msg.Id);
 | 
			
		||||
//                 try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task ListQueue(int page = 0)
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var (current, songs) = mp.QueueArray();
 | 
			
		||||
//
 | 
			
		||||
//             if (!songs.Any())
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyErrorLocalizedAsync("no_player").ConfigureAwait(false);
 | 
			
		||||
//                 return;
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             if (--page < -1)
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { }
 | 
			
		||||
//
 | 
			
		||||
//             const int itemsPerPage = 10;
 | 
			
		||||
//
 | 
			
		||||
//             if (page == -1)
 | 
			
		||||
//                 page = current / itemsPerPage;
 | 
			
		||||
//
 | 
			
		||||
//             //if page is 0 (-1 after this decrement) that means default to the page current song is playing from
 | 
			
		||||
//             var total = mp.TotalPlaytime;
 | 
			
		||||
//             var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format",
 | 
			
		||||
//                 (int)total.TotalHours,
 | 
			
		||||
//                 total.Minutes,
 | 
			
		||||
//                 total.Seconds);
 | 
			
		||||
//             var maxPlaytime = mp.MaxPlaytimeSeconds;
 | 
			
		||||
//
 | 
			
		||||
//             EmbedBuilder printAction(int curPage)
 | 
			
		||||
//             {
 | 
			
		||||
//                 var startAt = itemsPerPage * curPage;
 | 
			
		||||
//                 var number = 0 + startAt;
 | 
			
		||||
//                 var desc = string.Join("\n", songs
 | 
			
		||||
//                         .Skip(startAt)
 | 
			
		||||
//                         .Take(itemsPerPage)
 | 
			
		||||
//                         .Select(v =>
 | 
			
		||||
//                         {
 | 
			
		||||
//                             if (number++ == current)
 | 
			
		||||
//                                 return $"**⇒**`{number}.` {v.PrettyFullName}";
 | 
			
		||||
//                             else
 | 
			
		||||
//                                 return $"`{number}.` {v.PrettyFullName}";
 | 
			
		||||
//                         }));
 | 
			
		||||
//
 | 
			
		||||
//                 desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc;
 | 
			
		||||
//
 | 
			
		||||
//                 var add = "";
 | 
			
		||||
//                 if (mp.Stopped)
 | 
			
		||||
//                     add += Format.Bold(GetText("queue_stopped", Format.Code(Prefix + "play"))) + "\n";
 | 
			
		||||
//                 var mps = mp.MaxPlaytimeSeconds;
 | 
			
		||||
//                 if (mps > 0)
 | 
			
		||||
//                     add += Format.Bold(GetText("song_skips_after", TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss"))) + "\n";
 | 
			
		||||
//                 if (mp.RepeatCurrentSong)
 | 
			
		||||
//                     add += "🔂 " + GetText("repeating_cur_song") + "\n";
 | 
			
		||||
//                 else if (mp.Shuffle)
 | 
			
		||||
//                     add += "🔀 " + GetText("shuffling_playlist") + "\n";
 | 
			
		||||
//                 else
 | 
			
		||||
//                 {
 | 
			
		||||
//                     if (mp.Autoplay)
 | 
			
		||||
//                         add += "↪ " + GetText("autoplaying") + "\n";
 | 
			
		||||
//                     if (mp.FairPlay && !mp.Autoplay)
 | 
			
		||||
//                         add += " " + GetText("fairplay") + "\n";
 | 
			
		||||
//                     else if (mp.RepeatPlaylist)
 | 
			
		||||
//                         add += "🔁 " + GetText("repeating_playlist") + "\n";
 | 
			
		||||
//                 }
 | 
			
		||||
//
 | 
			
		||||
//                 if (!string.IsNullOrWhiteSpace(add))
 | 
			
		||||
//                     desc = add + "\n" + desc;
 | 
			
		||||
//
 | 
			
		||||
//                 var embed = new EmbedBuilder()
 | 
			
		||||
//                     .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, (songs.Length / itemsPerPage) + 1))
 | 
			
		||||
//                         .WithMusicIcon())
 | 
			
		||||
//                     .WithDescription(desc)
 | 
			
		||||
//                     .WithFooter(ef => ef.WithText($"{mp.PrettyVolume} | {songs.Length} " +
 | 
			
		||||
//                                                   $"{("tracks".SnPl(songs.Length))} | {totalStr}"))
 | 
			
		||||
//                     .WithOkColor();
 | 
			
		||||
//
 | 
			
		||||
//                 return embed;
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             await ctx.SendPaginatedConfirmAsync(page, printAction, songs.Length,
 | 
			
		||||
//                 itemsPerPage, false).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Next(int skipCount = 1)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (skipCount < 1)
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             mp.Next(skipCount);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Stop()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             mp.Stop();
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task AutoDisconnect()
 | 
			
		||||
//         {
 | 
			
		||||
//             var newVal = _service.ToggleAutoDc(ctx.Guild.Id);
 | 
			
		||||
//
 | 
			
		||||
//             if (newVal)
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("autodc_enable").ConfigureAwait(false);
 | 
			
		||||
//             else
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("autodc_disable").ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Destroy()
 | 
			
		||||
//         {
 | 
			
		||||
//             await _service.DestroyPlayer(ctx.Guild.Id).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Pause()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             mp.TogglePause();
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Volume(int val)
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             if (val < 0 || val > 100)
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyErrorLocalizedAsync("volume_input_invalid").ConfigureAwait(false);
 | 
			
		||||
//                 return;
 | 
			
		||||
//             }
 | 
			
		||||
//             mp.SetVolume(val);
 | 
			
		||||
//             await ReplyConfirmLocalizedAsync("volume_set", val).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Defvol([Leftover] int val)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (val < 0 || val > 100)
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyErrorLocalizedAsync("volume_input_invalid").ConfigureAwait(false);
 | 
			
		||||
//                 return;
 | 
			
		||||
//             }
 | 
			
		||||
//             using (var uow = _db.GetDbContext())
 | 
			
		||||
//             {
 | 
			
		||||
//                 uow.GuildConfigs.ForId(ctx.Guild.Id, set => set).DefaultMusicVolume = val / 100.0f;
 | 
			
		||||
//                 uow.SaveChanges();
 | 
			
		||||
//             }
 | 
			
		||||
//             await ReplyConfirmLocalizedAsync("defvol_set", val).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         [Priority(1)]
 | 
			
		||||
//         public async Task SongRemove(int index)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (index < 1)
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyErrorLocalizedAsync("removed_song_error").ConfigureAwait(false);
 | 
			
		||||
//                 return;
 | 
			
		||||
//             }
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             try
 | 
			
		||||
//             {
 | 
			
		||||
//                 var song = mp.RemoveAt(index - 1);
 | 
			
		||||
//                 var embed = new EmbedBuilder()
 | 
			
		||||
//                             .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index)).WithMusicIcon())
 | 
			
		||||
//                             .WithDescription(song.PrettyName)
 | 
			
		||||
//                             .WithFooter(ef => ef.WithText(song.PrettyInfo))
 | 
			
		||||
//                             .WithErrorColor();
 | 
			
		||||
//
 | 
			
		||||
//                 await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//             catch (ArgumentOutOfRangeException)
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyErrorLocalizedAsync("removed_song_error").ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         public enum All { All }
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         [Priority(0)]
 | 
			
		||||
//         public async Task SongRemove(All _)
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = _service.GetPlayerOrDefault(ctx.Guild.Id);
 | 
			
		||||
//             if (mp == null)
 | 
			
		||||
//                 return;
 | 
			
		||||
//             mp.Stop(true);
 | 
			
		||||
//             await ReplyConfirmLocalizedAsync("queue_cleared").ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Playlists([Leftover] int num = 1)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (num <= 0)
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             List<MusicPlaylist> playlists;
 | 
			
		||||
//
 | 
			
		||||
//             using (var uow = _db.GetDbContext())
 | 
			
		||||
//             {
 | 
			
		||||
//                 playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num);
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             var embed = new EmbedBuilder()
 | 
			
		||||
//                 .WithAuthor(eab => eab.WithName(GetText("playlists_page", num)).WithMusicIcon())
 | 
			
		||||
//                 .WithDescription(string.Join("\n", playlists.Select(r =>
 | 
			
		||||
//                     GetText("playlists", r.Id, r.Name, r.Author, r.Songs.Count))))
 | 
			
		||||
//                 .WithOkColor();
 | 
			
		||||
//             await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task DeletePlaylist([Leftover] int id)
 | 
			
		||||
//         {
 | 
			
		||||
//             var success = false;
 | 
			
		||||
//             try
 | 
			
		||||
//             {
 | 
			
		||||
//                 using (var uow = _db.GetDbContext())
 | 
			
		||||
//                 {
 | 
			
		||||
//                     var pl = uow.MusicPlaylists.GetById(id);
 | 
			
		||||
//
 | 
			
		||||
//                     if (pl != null)
 | 
			
		||||
//                     {
 | 
			
		||||
//                         if (_creds.IsOwner(ctx.User) || pl.AuthorId == ctx.User.Id)
 | 
			
		||||
//                         {
 | 
			
		||||
//                             uow.MusicPlaylists.Remove(pl);
 | 
			
		||||
//                             await uow.SaveChangesAsync();
 | 
			
		||||
//                             success = true;
 | 
			
		||||
//                         }
 | 
			
		||||
//                     }
 | 
			
		||||
//                 }
 | 
			
		||||
//             }
 | 
			
		||||
//             catch (Exception ex)
 | 
			
		||||
//             {
 | 
			
		||||
//                 Log.Warning(ex, "Error deleting playlist");
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             if (!success)
 | 
			
		||||
//                 await ReplyErrorLocalizedAsync("playlist_delete_fail").ConfigureAwait(false);
 | 
			
		||||
//             else
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("playlist_deleted").ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task PlaylistShow(int id, int page = 1)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (page-- < 1)
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             MusicPlaylist mpl;
 | 
			
		||||
//             using (var uow = _db.GetDbContext())
 | 
			
		||||
//             {
 | 
			
		||||
//                 mpl = uow.MusicPlaylists.GetWithSongs(id);
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             await ctx.SendPaginatedConfirmAsync(page, (cur) =>
 | 
			
		||||
//             {
 | 
			
		||||
//                 var i = 0;
 | 
			
		||||
//                 var str = string.Join("\n", mpl.Songs
 | 
			
		||||
//                     .Skip(cur * 20)
 | 
			
		||||
//                     .Take(20)
 | 
			
		||||
//                     .Select(x => $"`{++i}.` [{x.Title.TrimTo(45)}]({x.Query}) `{x.Provider}`"));
 | 
			
		||||
//                 return new EmbedBuilder()
 | 
			
		||||
//                     .WithTitle($"\"{mpl.Name}\" by {mpl.Author}")
 | 
			
		||||
//                     .WithOkColor()
 | 
			
		||||
//                     .WithDescription(str);
 | 
			
		||||
//             }, mpl.Songs.Count, 20).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Save([Leftover] string name)
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             var songs = mp.QueueArray().Songs
 | 
			
		||||
//                 .Select(s => new PlaylistSong()
 | 
			
		||||
//                 {
 | 
			
		||||
//                     Provider = s.Provider,
 | 
			
		||||
//                     ProviderType = s.ProviderType,
 | 
			
		||||
//                     Title = s.Title,
 | 
			
		||||
//                     Query = s.Query,
 | 
			
		||||
//                 }).ToList();
 | 
			
		||||
//
 | 
			
		||||
//             MusicPlaylist playlist;
 | 
			
		||||
//             using (var uow = _db.GetDbContext())
 | 
			
		||||
//             {
 | 
			
		||||
//                 playlist = new MusicPlaylist
 | 
			
		||||
//                 {
 | 
			
		||||
//                     Name = name,
 | 
			
		||||
//                     Author = ctx.User.Username,
 | 
			
		||||
//                     AuthorId = ctx.User.Id,
 | 
			
		||||
//                     Songs = songs.ToList(),
 | 
			
		||||
//                 };
 | 
			
		||||
//                 uow.MusicPlaylists.Add(playlist);
 | 
			
		||||
//                 await uow.SaveChangesAsync();
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
//                 .WithTitle(GetText("playlist_saved"))
 | 
			
		||||
//                 .AddField(efb => efb.WithName(GetText("name")).WithValue(name))
 | 
			
		||||
//                 .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString()))).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         private static readonly ConcurrentHashSet<ulong> PlaylistLoadBlacklist = new ConcurrentHashSet<ulong>();
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Load([Leftover] int id)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (!PlaylistLoadBlacklist.Add(ctx.Guild.Id))
 | 
			
		||||
//                 return;
 | 
			
		||||
//             try
 | 
			
		||||
//             {
 | 
			
		||||
//                 var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//                 MusicPlaylist mpl;
 | 
			
		||||
//                 using (var uow = _db.GetDbContext())
 | 
			
		||||
//                 {
 | 
			
		||||
//                     mpl = uow.MusicPlaylists.GetWithSongs(id);
 | 
			
		||||
//                 }
 | 
			
		||||
//
 | 
			
		||||
//                 if (mpl == null)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     await ReplyErrorLocalizedAsync("playlist_id_not_found").ConfigureAwait(false);
 | 
			
		||||
//                     return;
 | 
			
		||||
//                 }
 | 
			
		||||
//                 IUserMessage msg = null;
 | 
			
		||||
//                 try
 | 
			
		||||
//                 {
 | 
			
		||||
//                     msg = await ctx.Channel
 | 
			
		||||
//                         .SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString())))
 | 
			
		||||
//                         .ConfigureAwait(false);
 | 
			
		||||
//                 }
 | 
			
		||||
//                 catch (Exception)
 | 
			
		||||
//                 {
 | 
			
		||||
//                 }
 | 
			
		||||
//
 | 
			
		||||
//                 foreach (var item in mpl.Songs)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     try
 | 
			
		||||
//                     {
 | 
			
		||||
//                         await Task.Yield();
 | 
			
		||||
//                         var song = await _service.ResolveSong(item.Query,
 | 
			
		||||
//                             ctx.User.ToString(),
 | 
			
		||||
//                             item.ProviderType).ConfigureAwait(false);
 | 
			
		||||
//                         var queueTask = InternalQueue(mp, song, true);
 | 
			
		||||
//                         await Task.WhenAll(Task.Delay(1000), queueTask).ConfigureAwait(false);
 | 
			
		||||
//                     }
 | 
			
		||||
//                     catch (SongNotFoundException) { }
 | 
			
		||||
//                     catch { break; }
 | 
			
		||||
//                 }
 | 
			
		||||
//                 if (msg != null)
 | 
			
		||||
//                     await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//             finally
 | 
			
		||||
//             {
 | 
			
		||||
//                 PlaylistLoadBlacklist.TryRemove(ctx.Guild.Id);
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Fairplay()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var val = mp.FairPlay = !mp.FairPlay;
 | 
			
		||||
//
 | 
			
		||||
//             if (val)
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("fp_enabled").ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//             else
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("fp_disabled").ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task SongAutoDelete()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var val = mp.AutoDelete = !mp.AutoDelete;
 | 
			
		||||
//
 | 
			
		||||
//             _service.SetSongAutoDelete(ctx.Guild.Id, val);
 | 
			
		||||
//             if (val)
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("sad_enabled").ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//             else
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("sad_disabled").ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task SoundCloudQueue([Leftover] string query)
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var song = await _service.ResolveSong(query, ctx.User.ToString(), MusicType.Soundcloud).ConfigureAwait(false);
 | 
			
		||||
//             await InternalQueue(mp, song, false).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task SoundCloudPl([Leftover] string pl)
 | 
			
		||||
//         {
 | 
			
		||||
//             pl = pl?.Trim();
 | 
			
		||||
//
 | 
			
		||||
//             if (string.IsNullOrWhiteSpace(pl))
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             using (var http = new HttpClient())
 | 
			
		||||
//             {
 | 
			
		||||
//                 var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject<SoundCloudVideo[]>();
 | 
			
		||||
//                 IUserMessage msg = null;
 | 
			
		||||
//                 try { msg = await ctx.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(scvids.Length.ToString()))).ConfigureAwait(false); } catch { }
 | 
			
		||||
//                 foreach (var svideo in scvids)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     try
 | 
			
		||||
//                     {
 | 
			
		||||
//                         var sinfo = await svideo.GetSongInfo().ConfigureAwait(false);
 | 
			
		||||
//                         sinfo.QueuerName = ctx.User.ToString();
 | 
			
		||||
//                         await InternalQueue(mp, sinfo, true).ConfigureAwait(false);
 | 
			
		||||
//                     }
 | 
			
		||||
//                     catch (Exception ex)
 | 
			
		||||
//                     {
 | 
			
		||||
//                         Log.Warning(ex, "Error queueing soundcloud song: {Title}", svideo.Title);
 | 
			
		||||
//                         break;
 | 
			
		||||
//                     }
 | 
			
		||||
//                 }
 | 
			
		||||
//                 if (msg != null)
 | 
			
		||||
//                     await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false);
 | 
			
		||||
//             }
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task NowPlaying()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var (_, currentSong) = mp.Current;
 | 
			
		||||
//             if (currentSong == null)
 | 
			
		||||
//                 return;
 | 
			
		||||
//             try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { }
 | 
			
		||||
//
 | 
			
		||||
//             var embed = new EmbedBuilder().WithOkColor()
 | 
			
		||||
//                             .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon())
 | 
			
		||||
//                             .WithDescription(currentSong.PrettyName)
 | 
			
		||||
//                             .WithThumbnailUrl(currentSong.Thumbnail)
 | 
			
		||||
//                             .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + mp.PrettyFullTime + $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}"));
 | 
			
		||||
//
 | 
			
		||||
//             await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task PlaylistShuffle()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var val = mp.ToggleShuffle();
 | 
			
		||||
//             if (val)
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("songs_shuffle_enable").ConfigureAwait(false);
 | 
			
		||||
//             else
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("songs_shuffle_disable").ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Playlist([Leftover] string playlist)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (string.IsNullOrWhiteSpace(playlist))
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             string plId = null;
 | 
			
		||||
//             try
 | 
			
		||||
//             {
 | 
			
		||||
//                 plId = (await _google.GetPlaylistIdsByKeywordsAsync(playlist).ConfigureAwait(false)).FirstOrDefault();
 | 
			
		||||
//             }
 | 
			
		||||
//             catch (Exception ex)
 | 
			
		||||
//             {
 | 
			
		||||
//                 Log.Warning(ex.Message);
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             if (plId == null)
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyErrorLocalizedAsync("no_search_results").ConfigureAwait(false);
 | 
			
		||||
//                 return;
 | 
			
		||||
//             }
 | 
			
		||||
//             var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false);
 | 
			
		||||
//             if (!ids.Any())
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyErrorLocalizedAsync("no_search_results").ConfigureAwait(false);
 | 
			
		||||
//                 return;
 | 
			
		||||
//             }
 | 
			
		||||
//             var count = ids.Count();
 | 
			
		||||
//             var msg = await ctx.Channel.SendMessageAsync("🎵 " + GetText("attempting_to_queue",
 | 
			
		||||
//                 Format.Bold(count.ToString()))).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             foreach (var song in ids)
 | 
			
		||||
//             {
 | 
			
		||||
//                 try
 | 
			
		||||
//                 {
 | 
			
		||||
//                     if (mp.Exited)
 | 
			
		||||
//                         return;
 | 
			
		||||
//
 | 
			
		||||
//                     await Task.WhenAll(Task.Delay(150), InternalQueue(mp, await _service.ResolveSong(song, ctx.User.ToString(), MusicType.YouTube).ConfigureAwait(false), true)).ConfigureAwait(false);
 | 
			
		||||
//                 }
 | 
			
		||||
//                 catch (SongNotFoundException) { }
 | 
			
		||||
//                 catch { break; }
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Radio(string radioLink)
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var song = await _service.ResolveSong(radioLink, ctx.User.ToString(), MusicType.Radio).ConfigureAwait(false);
 | 
			
		||||
//             await InternalQueue(mp, song, false).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         [OwnerOnly]
 | 
			
		||||
//         public async Task Local([Leftover] string path)
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var song = await _service.ResolveSong(path, ctx.User.ToString(), MusicType.Local).ConfigureAwait(false);
 | 
			
		||||
//             await InternalQueue(mp, song, false).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         [OwnerOnly]
 | 
			
		||||
//         public async Task LocalPlaylist([Leftover] string dirPath)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (string.IsNullOrWhiteSpace(dirPath))
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             DirectoryInfo dir;
 | 
			
		||||
//             try { dir = new DirectoryInfo(dirPath); } catch { return; }
 | 
			
		||||
//             var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories)
 | 
			
		||||
//                                 .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System) && x.Extension != ".jpg" && x.Extension != ".png");
 | 
			
		||||
//             foreach (var file in fileEnum)
 | 
			
		||||
//             {
 | 
			
		||||
//                 try
 | 
			
		||||
//                 {
 | 
			
		||||
//                     var song = await _service.ResolveSong(file.FullName, ctx.User.ToString(), MusicType.Local).ConfigureAwait(false);
 | 
			
		||||
//                     await InternalQueue(mp, song, true).ConfigureAwait(false);
 | 
			
		||||
//                 }
 | 
			
		||||
//                 catch (QueueFullException)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     break;
 | 
			
		||||
//                 }
 | 
			
		||||
//                 catch (Exception ex)
 | 
			
		||||
//                 {
 | 
			
		||||
//                     Log.Warning(ex, "Error resolving local song {FileName}", file);
 | 
			
		||||
//                     break;
 | 
			
		||||
//                 }
 | 
			
		||||
//             }
 | 
			
		||||
//             await ReplyConfirmLocalizedAsync("dir_queue_complete").ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Move()
 | 
			
		||||
//         {
 | 
			
		||||
//             var vch = ((IGuildUser)ctx.User).VoiceChannel;
 | 
			
		||||
//
 | 
			
		||||
//             if (vch == null)
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             var mp = _service.GetPlayerOrDefault(ctx.Guild.Id);
 | 
			
		||||
//
 | 
			
		||||
//             if (mp == null)
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             await mp.SetVoiceChannel(vch).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task MoveSong([Leftover] string fromto)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (string.IsNullOrWhiteSpace(fromto))
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             MusicPlayer mp = _service.GetPlayerOrDefault(ctx.Guild.Id);
 | 
			
		||||
//             if (mp == null)
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             fromto = fromto?.Trim();
 | 
			
		||||
//             var fromtoArr = fromto.Split('>');
 | 
			
		||||
//
 | 
			
		||||
//             SongInfo s;
 | 
			
		||||
//             if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out var n1) ||
 | 
			
		||||
//                 !int.TryParse(fromtoArr[1], out var n2) || n1 < 1 || n2 < 1 || n1 == n2
 | 
			
		||||
//                 || (s = mp.MoveSong(--n1, --n2)) == null)
 | 
			
		||||
//             {
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("invalid_input").ConfigureAwait(false);
 | 
			
		||||
//                 return;
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             var embed = new EmbedBuilder()
 | 
			
		||||
//                 .WithTitle(s.Title.TrimTo(65))
 | 
			
		||||
//                 .WithUrl(s.SongUrl)
 | 
			
		||||
//                 .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png"))
 | 
			
		||||
//                 .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1 + 1}").WithIsInline(true))
 | 
			
		||||
//                 .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2 + 1}").WithIsInline(true))
 | 
			
		||||
//                 .WithColor(NadekoBot.OkColor);
 | 
			
		||||
//
 | 
			
		||||
//             await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
         // }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task SetMaxQueue(uint size = 0)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (size < 0)
 | 
			
		||||
//                 return;
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             mp.MaxQueueSize = size;
 | 
			
		||||
//
 | 
			
		||||
//             if (size == 0)
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("max_queue_unlimited").ConfigureAwait(false);
 | 
			
		||||
//             else
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("max_queue_x", size).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task SetMaxPlaytime(uint seconds)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (seconds < 15 && seconds != 0)
 | 
			
		||||
//                 return;
 | 
			
		||||
//
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             mp.MaxPlaytimeSeconds = seconds;
 | 
			
		||||
//             if (seconds == 0)
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("max_playtime_none").ConfigureAwait(false);
 | 
			
		||||
//             else
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("max_playtime_set", seconds).ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task ReptCurSong()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var (_, currentSong) = mp.Current;
 | 
			
		||||
//             if (currentSong == null)
 | 
			
		||||
//                 return;
 | 
			
		||||
//             var currentValue = mp.ToggleRepeatSong();
 | 
			
		||||
//
 | 
			
		||||
//             if (currentValue)
 | 
			
		||||
//                 await ctx.Channel.EmbedAsync(new EmbedBuilder()
 | 
			
		||||
//                     .WithOkColor()
 | 
			
		||||
//                     .WithAuthor(eab => eab.WithMusicIcon().WithName("🔂 " + GetText("repeating_track")))
 | 
			
		||||
//                     .WithDescription(currentSong.PrettyName)
 | 
			
		||||
//                     .WithFooter(ef => ef.WithText(currentSong.PrettyInfo))).ConfigureAwait(false);
 | 
			
		||||
//             else
 | 
			
		||||
//                 await ctx.Channel.SendConfirmAsync("🔂 " + GetText("repeating_track_stopped"))
 | 
			
		||||
//                                             .ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task PlaylistRepeat()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//             var currentValue = mp.ToggleRepeatPlaylist();
 | 
			
		||||
//             if (currentValue)
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("rpl_enabled").ConfigureAwait(false);
 | 
			
		||||
//             else
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("rpl_disabled").ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         public async Task Autoplay()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             if (!mp.ToggleAutoplay())
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("autoplay_disabled").ConfigureAwait(false);
 | 
			
		||||
//             else
 | 
			
		||||
//                 await ReplyConfirmLocalizedAsync("autoplay_enabled").ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
//         public async Task SetMusicChannel()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             mp.OutputTextChannel = (ITextChannel)ctx.Channel;
 | 
			
		||||
//             _service.SetMusicChannel(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
//
 | 
			
		||||
//             await ReplyConfirmLocalizedAsync("set_music_channel").ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//         [RequireContext(ContextType.Guild)]
 | 
			
		||||
//         [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
//         public async Task UnsetMusicChannel()
 | 
			
		||||
//         {
 | 
			
		||||
//             var mp = await _service.GetOrCreatePlayer(Context).ConfigureAwait(false);
 | 
			
		||||
//
 | 
			
		||||
//             mp.OutputTextChannel = mp.OriginalTextChannel;
 | 
			
		||||
//             _service.SetMusicChannel(ctx.Guild.Id, null);
 | 
			
		||||
//
 | 
			
		||||
//             await ReplyConfirmLocalizedAsync("unset_music_channel").ConfigureAwait(false);
 | 
			
		||||
//         }
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
		Reference in New Issue
	
	Block a user