mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Killed history
This commit is contained in:
14
NadekoBot.Core/Modules/Music/Common/ICachableTrackData.cs
Normal file
14
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/IMusicPlayer.cs
Normal file
39
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/IMusicQueue.cs
Normal file
27
NadekoBot.Core/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();
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Music
|
||||
{
|
||||
public interface IPlatformQueryResolver
|
||||
{
|
||||
Task<ITrackInfo?> ResolveByQueryAsync(string query);
|
||||
}
|
||||
}
|
9
NadekoBot.Core/Modules/Music/Common/IQueuedTrackInfo.cs
Normal file
9
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/IRadioResolver.cs
Normal file
7
NadekoBot.Core/Modules/Music/Common/IRadioResolver.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Core.Modules.Music
|
||||
{
|
||||
public interface IRadioResolver : IPlatformQueryResolver
|
||||
{
|
||||
|
||||
}
|
||||
}
|
10
NadekoBot.Core/Modules/Music/Common/ISoundcloudResolver.cs
Normal file
10
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/ITrackCacher.cs
Normal file
26
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/ITrackInfo.cs
Normal file
16
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/ITrackResolveProvider.cs
Normal file
10
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/IVoiceProxy.cs
Normal file
17
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/IYoutubeResolver.cs
Normal file
15
NadekoBot.Core/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);
|
||||
}
|
||||
}
|
@@ -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
NadekoBot.Core/Modules/Music/Common/Impl/MultimediaTimer.cs
Normal file
92
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/Impl/MusicExtensions.cs
Normal file
61
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/Impl/MusicPlatform.cs
Normal file
10
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/Impl/MusicPlayer.cs
Normal file
540
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/Impl/MusicQueue.cs
Normal file
323
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/Impl/RedisTrackCacher.cs
Normal file
213
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/Impl/RemoteTrackInfo.cs
Normal file
30
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/Impl/SimpleTrackInfo.cs
Normal file
28
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/Impl/VoiceProxy.cs
Normal file
119
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/_OldMusicPlayer.cs
Normal file
717
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Common/_OldMusicQueue.cs
Normal file
223
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Music.cs
Normal file
783
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/PlaylistCommands.cs
Normal file
247
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Services/AyuVoiceStateService.cs
Normal file
221
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Services/IMusicService.cs
Normal file
41
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Services/MusicService.cs
Normal file
457
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Services/_OldMusicService.cs
Normal file
297
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Services/extractor/Misc.cs
Normal file
75
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/Services/extractor/YtLoader.cs
Normal file
140
NadekoBot.Core/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
NadekoBot.Core/Modules/Music/_OldMusic.cs
Normal file
984
NadekoBot.Core/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