Applied codestyle to all .cs files

This commit is contained in:
Kwoth
2021-12-29 06:07:16 +01:00
parent 723447c7d4
commit 82000c97a4
543 changed files with 13221 additions and 14059 deletions

View File

@@ -9,4 +9,4 @@ public interface ICachableTrackData
public TimeSpan Duration { get; }
MusicPlatform Platform { get; set; }
string Title { get; set; }
}
}

View File

@@ -4,4 +4,4 @@ namespace NadekoBot.Modules.Music;
public interface ILocalTrackResolver : IPlatformQueryResolver
{
IAsyncEnumerable<ITrackInfo> ResolveDirectoryAsync(string dirPath);
}
}

View File

@@ -21,9 +21,14 @@ public interface IMusicPlayer : IDisposable
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<(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);
@@ -31,4 +36,4 @@ public interface IMusicPlayer : IDisposable
void EnqueueTracks(IEnumerable<ITrackInfo> tracks, string queuer);
void SetRepeat(PlayerRepeatType type);
void ShuffleQueue();
}
}

View File

@@ -2,21 +2,21 @@
public interface IMusicQueue
{
int Index { get; }
int Count { get; }
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();
}
}

View File

@@ -3,4 +3,4 @@
public interface IPlatformQueryResolver
{
Task<ITrackInfo?> ResolveByQueryAsync(string query);
}
}

View File

@@ -6,4 +6,4 @@ public interface IQueuedTrackInfo : ITrackInfo
public ITrackInfo TrackInfo { get; }
public string Queuer { get; }
}
}

View File

@@ -3,5 +3,4 @@ namespace NadekoBot.Modules.Music;
public interface IRadioResolver : IPlatformQueryResolver
{
}
}

View File

@@ -5,4 +5,4 @@ public interface ISoundcloudResolver : IPlatformQueryResolver
{
bool IsSoundCloudLink(string url);
IAsyncEnumerable<ITrackInfo> ResolvePlaylistAsync(string playlist);
}
}

View File

@@ -5,16 +5,21 @@ public interface ITrackCacher
Task<string?> GetOrCreateStreamLink(
string id,
MusicPlatform platform,
Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory
);
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 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);
}
}

View File

@@ -8,4 +8,4 @@ public interface ITrackInfo
public TimeSpan Duration { get; }
public MusicPlatform Platform { get; }
public ValueTask<string?> GetStreamUrl();
}
}

View File

@@ -3,4 +3,4 @@
public interface ITrackResolveProvider
{
Task<ITrackInfo?> QuerySongAsync(string query, MusicPlatform? forcePlatform);
}
}

View File

@@ -12,4 +12,4 @@ public interface IVoiceProxy
Task StopSpeakingAsync();
public Task StartGateway();
Task StopGateway();
}
}

View File

@@ -8,4 +8,4 @@ public interface IYoutubeResolver : IPlatformQueryResolver
public Task<ITrackInfo?> ResolveByIdAsync(string id);
IAsyncEnumerable<ITrackInfo> ResolveTracksFromPlaylistAsync(string query);
Task<ITrackInfo?> ResolveByQueryAsync(string query, bool tryExtractingId);
}
}

View File

@@ -10,7 +10,10 @@ public sealed class CachableTrackData : ICachableTrackData
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 TimeSpan Duration
=> TimeSpan.FromMilliseconds(TotalDurationMs);
public MusicPlatform Platform { get; set; }
}
}

View File

@@ -5,61 +5,6 @@ namespace NadekoBot.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;
@@ -77,7 +22,56 @@ public sealed class MultimediaTimer : IDisposable
_eventId = timeSetEvent((uint)period, 1, _lpTimeProc, 0, TimerMode.Periodic);
}
private void CallbackInternal(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);
/// <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 void CallbackInternal(
uint uTimerId,
uint uMsg,
int dwUser,
int dw1,
int dw2)
=> _callback(_state);
public void Dispose()
@@ -85,4 +79,17 @@ public sealed class MultimediaTimer : IDisposable
_lpTimeProc = default;
timeKillEvent(_eventId);
}
}
private delegate void LpTimeProcDelegate(
uint uTimerID,
uint uMsg,
int dwUser,
int dw1,
int dw2);
private enum TimerMode
{
OneShot,
Periodic
}
}

View File

@@ -20,8 +20,8 @@ public static class MusicExtensions
}
public static string PrettyVolume(this IMusicPlayer mp)
=> $"🔉 {(int) (mp.Volume * 100)}%";
=> $"🔉 {(int)(mp.Volume * 100)}%";
public static string PrettyName(this ITrackInfo trackInfo)
=> $"**[{trackInfo.Title.TrimTo(60).Replace("[", "\\[").Replace("]", "\\]")}]({trackInfo.Url.TrimTo(50, true)})**";
@@ -45,7 +45,7 @@ public static class MusicExtensions
}
public static ICachableTrackData ToCachedData(this ITrackInfo trackInfo, string id)
=> new CachableTrackData()
=> new CachableTrackData
{
TotalDurationMs = trackInfo.Duration.TotalMilliseconds,
Id = id,
@@ -54,4 +54,4 @@ public static class MusicExtensions
Platform = trackInfo.Platform,
Title = trackInfo.Title
};
}
}

View File

@@ -6,5 +6,5 @@ public enum MusicPlatform
Radio,
Youtube,
Local,
SoundCloud,
}
SoundCloud
}

View File

@@ -1,28 +1,29 @@
using Ayu.Discord.Voice;
using NadekoBot.Services.Database.Models;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Ayu.Discord.Voice;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Music;
public sealed class MusicPlayer : IMusicPlayer
{
private delegate void AdjustVolumeDelegate(Span<byte> data, float volume);
private readonly AdjustVolumeDelegate AdjustVolume;
private readonly VoiceClient _vc;
public event Func<IMusicPlayer, IQueuedTrackInfo, Task>? OnCompleted;
public event Func<IMusicPlayer, IQueuedTrackInfo, int, Task>? OnStarted;
public event Func<IMusicPlayer, Task>? OnQueueStopped;
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;
public int CurrentIndex
=> _queue.Index;
public float Volume { get; private set; } = 1.0f;
private readonly AdjustVolumeDelegate AdjustVolume;
private readonly VoiceClient _vc;
private readonly IMusicQueue _queue;
private readonly ITrackResolveProvider _trackResolveProvider;
@@ -50,7 +51,7 @@ public sealed class MusicPlayer : IMusicPlayer
AdjustVolume = AdjustVolumeInt16;
else
AdjustVolume = AdjustVolumeFloat32;
_songBuffer = new PoopyBufferImmortalized(_vc.InputLength);
_thread = new(async () =>
@@ -63,34 +64,18 @@ public sealed class MusicPlayer : IMusicPlayer
private static VoiceClient GetVoiceClient(QualityPreset qualityPreset)
=> qualityPreset switch
{
QualityPreset.Highest => new(
SampleRate._48k,
Bitrate._192k,
Channels.Two,
FrameDelay.Delay20,
BitDepthEnum.Float32
),
QualityPreset.High => new(
SampleRate._48k,
Bitrate._128k,
Channels.Two,
FrameDelay.Delay40,
BitDepthEnum.Float32
),
QualityPreset.Medium => new(
SampleRate._48k,
QualityPreset.Highest => new(),
QualityPreset.High => new(SampleRate._48k, Bitrate._128k, Channels.Two, FrameDelay.Delay40),
QualityPreset.Medium => new(SampleRate._48k,
Bitrate._96k,
Channels.Two,
FrameDelay.Delay40,
BitDepthEnum.UInt16
),
QualityPreset.Low => new(
SampleRate._48k,
BitDepthEnum.UInt16),
QualityPreset.Low => new(SampleRate._48k,
Bitrate._64k,
Channels.Two,
FrameDelay.Delay40,
BitDepthEnum.UInt16
),
BitDepthEnum.UInt16),
_ => throw new ArgumentOutOfRangeException(nameof(qualityPreset), qualityPreset, null)
};
@@ -103,7 +88,7 @@ public sealed class MusicPlayer : IMusicPlayer
// wait until a song is available in the queue
// or until the queue is resumed
var track = _queue.GetCurrent(out var index);
if (track is null || IsStopped)
{
await Task.Delay(500);
@@ -134,9 +119,8 @@ public sealed class MusicPlayer : IMusicPlayer
using var source = FfmpegTrackDataSource.CreateAsync(
_vc.BitDepth,
streamUrl,
track.Platform == MusicPlatform.Local
);
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);
@@ -191,10 +175,8 @@ public sealed class MusicPlayer : IMusicPlayer
sw.Start();
Thread.Sleep(2);
var delay = sw.ElapsedTicks * ticksPerMs > 3f
? _vc.Delay - 16
: _vc.Delay - 3;
var delay = sw.ElapsedTicks * ticksPerMs > 3f ? _vc.Delay - 16 : _vc.Delay - 3;
var errorCount = 0;
while (!IsStopped && !IsKilled)
{
@@ -205,23 +187,23 @@ public sealed class MusicPlayer : IMusicPlayer
_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)
@@ -229,12 +211,12 @@ public sealed class MusicPlayer : IMusicPlayer
_ = _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);
@@ -243,16 +225,16 @@ public sealed class MusicPlayer : IMusicPlayer
{
// 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
@@ -268,8 +250,8 @@ public sealed class MusicPlayer : IMusicPlayer
catch (Win32Exception)
{
IsStopped = true;
Log.Error("Please install ffmpeg and make sure it's added to your " +
"PATH environment variable before trying again");
Log.Error("Please install ffmpeg and make sure it's added to your "
+ "PATH environment variable before trying again");
}
catch (OperationCanceledException)
{
@@ -284,12 +266,13 @@ public sealed class MusicPlayer : IMusicPlayer
cancellationTokenSource.Cancel();
// turn off green in vc
_ = OnCompleted?.Invoke(this, track);
HandleQueuePostTrack();
_skipped = false;
_ = _proxy.StopSpeakingAsync();;
_ = _proxy.StopSpeakingAsync();
;
await Task.Delay(100);
}
}
@@ -300,12 +283,9 @@ public sealed class MusicPlayer : IMusicPlayer
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;
}
if (data.Length == 0) return null;
AdjustVolume(data, _volume);
AdjustVolume(data, Volume);
return _proxy.SendPcmFrame(vc, data, length);
}
@@ -322,7 +302,7 @@ public sealed class MusicPlayer : IMusicPlayer
if (repeat == PlayerRepeatType.Track || isStopped)
return;
// if queue is being repeated, advance no matter what
if (repeat == PlayerRepeatType.None)
{
@@ -334,37 +314,36 @@ public sealed class MusicPlayer : IMusicPlayer
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);
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];
@@ -373,7 +352,7 @@ public sealed class MusicPlayer : IMusicPlayer
}
public async Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync(
string query,
string query,
string queuer,
bool asNext,
MusicPlatform? forcePlatform = null)
@@ -389,7 +368,7 @@ public sealed class MusicPlayer : IMusicPlayer
return (_queue.Enqueue(song, queuer, out index), index);
}
public async Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer)
{
var errorCount = 0;
@@ -397,30 +376,31 @@ public sealed class MusicPlayer : IMusicPlayer
{
if (IsKilled)
break;
await chunk.Select(async data =>
{
var (query, platform) = data;
try
{
await TryEnqueueTrackAsync(query, queuer, false, forcePlatform: platform);
errorCount = 0;
}
catch (Exception ex)
{
Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query);
++errorCount;
}
}).WhenAll();
{
var (query, platform) = data;
try
{
await TryEnqueueTrackAsync(query, queuer, false, platform);
errorCount = 0;
}
catch (Exception ex)
{
Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query);
++errorCount;
}
})
.WhenAll();
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 _);
@@ -475,7 +455,7 @@ public sealed class MusicPlayer : IMusicPlayer
if (normalizedVolume is < 0f or > 1f)
throw new ArgumentOutOfRangeException(nameof(newVolume), "Volume must be in range 0-100");
_volume = normalizedVolume;
Volume = normalizedVolume;
}
public void Kill()
@@ -496,9 +476,12 @@ public sealed class MusicPlayer : IMusicPlayer
return true;
}
public bool TogglePause() => IsPaused = !IsPaused;
public IQueuedTrackInfo? MoveTrack(int from, int to) => _queue.MoveTrack(from, to);
public bool TogglePause()
=> IsPaused = !IsPaused;
public IQueuedTrackInfo? MoveTrack(int from, int to)
=> _queue.MoveTrack(from, to);
public void Dispose()
{
@@ -511,7 +494,5 @@ public sealed class MusicPlayer : IMusicPlayer
_vc.Dispose();
}
public event Func<IMusicPlayer, IQueuedTrackInfo, Task>? OnCompleted;
public event Func<IMusicPlayer, IQueuedTrackInfo, int, Task>? OnStarted;
public event Func<IMusicPlayer, Task>? OnQueueStopped;
}
private delegate void AdjustVolumeDelegate(Span<byte> data, float volume);
}

View File

@@ -7,11 +7,20 @@ public sealed partial class MusicQueue
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 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)
@@ -20,14 +29,13 @@ public sealed partial class MusicQueue
Queuer = queuer;
}
public ValueTask<string?> GetStreamUrl() => TrackInfo.GetStreamUrl();
public ValueTask<string?> GetStreamUrl()
=> TrackInfo.GetStreamUrl();
}
}
public sealed partial class MusicQueue : IMusicQueue
{
private LinkedList<QueuedTrackInfo> _tracks;
public int Index
{
get
@@ -41,8 +49,6 @@ public sealed partial class MusicQueue : IMusicQueue
}
}
private int _index;
public int Count
{
get
@@ -54,6 +60,10 @@ public sealed partial class MusicQueue : IMusicQueue
}
}
private LinkedList<QueuedTrackInfo> _tracks;
private int _index;
private readonly object locker = new();
public MusicQueue()
@@ -77,17 +87,12 @@ public sealed partial class MusicQueue : IMusicQueue
{
lock (locker)
{
if (_tracks.Count == 0)
{
return Enqueue(trackInfo, queuer, out index);
}
if (_tracks.Count == 0) return Enqueue(trackInfo, queuer, out index);
var 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;
@@ -160,10 +165,7 @@ public sealed partial class MusicQueue : IMusicQueue
{
var removedNode = _tracks.First!;
int i;
for (i = 0; i < index; i++)
{
removedNode = removedNode.Next!;
}
for (i = 0; i < index; i++) removedNode = removedNode.Next!;
trackInfo = removedNode.Value;
_tracks.Remove(removedNode);
@@ -303,14 +305,11 @@ public sealed partial class MusicQueue : IMusicQueue
if (index < 0 || index >= _tracks.Count)
return false;
if (index == _index)
{
isCurrent = true;
}
if (index == _index) isCurrent = true;
RemoveAtInternal(index, out trackInfo);
return true;
}
}
}
}

View File

@@ -1,6 +1,6 @@
using System.Runtime.CompilerServices;
using StackExchange.Redis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using StackExchange.Redis;
namespace NadekoBot.Modules.Music;
@@ -14,13 +14,12 @@ public sealed class RedisTrackCacher : ITrackCacher
public async Task<string?> GetOrCreateStreamLink(
string id,
MusicPlatform platform,
Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory
)
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)
{
@@ -28,17 +27,16 @@ public sealed class RedisTrackCacher : ITrackCacher
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)
@@ -64,7 +62,11 @@ public sealed class RedisTrackCacher : ITrackCacher
}
}
public Task CacheStreamUrlAsync(string id, MusicPlatform platform, string url, TimeSpan expiry)
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)
@@ -73,10 +75,10 @@ public sealed class RedisTrackCacher : ITrackCacher
// 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.StringSetAsync(dataKey, url, expiry);
await db.ListRightPushAsync(trackStreamKey, dataKey);
}
@@ -86,7 +88,7 @@ public sealed class RedisTrackCacher : ITrackCacher
// from the list of cached trackurls until it finds a non-expired key
var db = _multiplexer.GetDatabase();
while(true)
while (true)
{
string? dataKey = await db.ListLeftPopAsync(trackStreamKey);
if (dataKey == default)
@@ -103,7 +105,7 @@ public sealed class RedisTrackCacher : ITrackCacher
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string CreateCachedDataKey(string id, MusicPlatform platform)
=> $"track:data:{platform}:{id}";
public Task CacheTrackDataAsync(ICachableTrackData data)
{
var db = _multiplexer.GetDatabase();
@@ -111,13 +113,13 @@ public sealed class RedisTrackCacher : ITrackCacher
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));
return db.StringSetAsync(trackDataKey, dataString, 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)
@@ -129,10 +131,11 @@ public sealed class RedisTrackCacher : ITrackCacher
[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);
@@ -149,17 +152,18 @@ public sealed class RedisTrackCacher : ITrackCacher
// 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();
@@ -175,13 +179,14 @@ public sealed class RedisTrackCacher : ITrackCacher
{
var db = _multiplexer.GetDatabase();
var key = CreateCachedPlaylistKey(playlistId, platform);
await db.ListRightPushAsync(key, ids.Select(x => (RedisValue) x).ToArray());
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());
@@ -201,4 +206,4 @@ public sealed class RedisTrackCacher : ITrackCacher
return val;
}
}
}

View File

@@ -10,7 +10,12 @@ public sealed class RemoteTrackInfo : ITrackInfo
private readonly Func<Task<string?>> _streamFactory;
public RemoteTrackInfo(string title, string url, string thumbnail, TimeSpan duration, MusicPlatform platform,
public RemoteTrackInfo(
string title,
string url,
string thumbnail,
TimeSpan duration,
MusicPlatform platform,
Func<Task<string?>> streamFactory)
{
_streamFactory = streamFactory;
@@ -21,5 +26,6 @@ public sealed class RemoteTrackInfo : ITrackInfo
Platform = platform;
}
public async ValueTask<string?> GetStreamUrl() => await _streamFactory();
}
public async ValueTask<string?> GetStreamUrl()
=> await _streamFactory();
}

View File

@@ -8,10 +8,14 @@ public sealed class SimpleTrackInfo : ITrackInfo
public TimeSpan Duration { get; }
public MusicPlatform Platform { get; }
public string? StreamUrl { get; }
public ValueTask<string?> GetStreamUrl() => new(StreamUrl);
public SimpleTrackInfo(string title, string url, string thumbnail, TimeSpan duration,
MusicPlatform platform, string streamUrl)
public SimpleTrackInfo(
string title,
string url,
string thumbnail,
TimeSpan duration,
MusicPlatform platform,
string streamUrl)
{
Title = title;
Url = url;
@@ -20,4 +24,7 @@ public sealed class SimpleTrackInfo : ITrackInfo
Platform = platform;
StreamUrl = streamUrl;
}
}
public ValueTask<string?> GetStreamUrl()
=> new(StreamUrl);
}

View File

@@ -12,19 +12,19 @@ public sealed class VoiceProxy : IVoiceProxy
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,
{ Started: true, Stopped: false } => VoiceProxyState.Started,
{ Stopped: false } => VoiceProxyState.Created,
_ => VoiceProxyState.Stopped
};
private VoiceGateway _gateway;
public VoiceProxy(VoiceGateway initial)
@@ -35,10 +35,7 @@ public sealed class VoiceProxy : IVoiceProxy
try
{
var gw = _gateway;
if (gw is null || gw.Stopped || !gw.Started)
{
return false;
}
if (gw is null || gw.Stopped || !gw.Started) return false;
vc.SendPcmFrame(gw, data, 0, length);
return true;
@@ -54,10 +51,7 @@ public sealed class VoiceProxy : IVoiceProxy
var errorCount = 0;
do
{
if (State == VoiceProxyState.Stopped)
{
break;
}
if (State == VoiceProxyState.Stopped) break;
try
{
@@ -98,9 +92,9 @@ public sealed class VoiceProxy : IVoiceProxy
public Task StopGateway()
{
if(_gateway is { } gw)
if (_gateway is { } gw)
return gw.StopAsync();
return Task.CompletedTask;
}
}
}

View File

@@ -8,25 +8,22 @@ 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",
".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),
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)}\""
);
$"\"{Path.GetFullPath(query)}\"");
}
public async IAsyncEnumerable<ITrackInfo> ResolveDirectoryAsync(string dirPath)
@@ -43,17 +40,17 @@ public sealed class LocalTrackResolver : ILocalTrackResolver
}
var files = dir.EnumerateFiles()
.Where(x =>
{
if (!x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)
&& _musicExtensions.Contains(x.Extension.ToUpperInvariant())) return true;
return false;
});
.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 not null)
yield return firstData;
@@ -61,9 +58,8 @@ public sealed class LocalTrackResolver : ILocalTrackResolver
var fileChunks = files.Skip(1).Chunk(10);
foreach (var chunk in fileChunks)
{
var part = await chunk.Select(x => ResolveByQueryAsync(x.FullName))
.WhenAll();
var part = await chunk.Select(x => ResolveByQueryAsync(x.FullName)).WhenAll();
// nullable reference types being annoying
foreach (var p in part)
{
@@ -84,7 +80,7 @@ public static class Ffprobe
try
{
using var p = Process.Start(new ProcessStartInfo()
using var p = Process.Start(new ProcessStartInfo
{
FileName = "ffprobe",
Arguments =
@@ -94,7 +90,7 @@ public static class Ffprobe
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
CreateNoWindow = true,
CreateNoWindow = true
});
if (p is null)
@@ -118,7 +114,7 @@ public static class Ffprobe
{
Log.Error(ex, "Unknown exception running ffprobe; {ErrorMessage}", ex.Message);
}
return TimeSpan.Zero;
}
}
}

View File

@@ -10,33 +10,23 @@ public class RadioResolver : IRadioResolver
private readonly Regex asxRegex = new("<ref href=\"(?<url>.*?)\"", RegexOptions.Compiled);
private readonly Regex xspfRegex = new("<location>(?<url>.*?)</location>", RegexOptions.Compiled);
public RadioResolver()
{
}
public async Task<ITrackInfo> ResolveByQueryAsync(string query)
{
if (IsRadioLink(query))
query = await HandleStreamContainers(query);
return new SimpleTrackInfo(
query.TrimTo(50),
return new SimpleTrackInfo(query.TrimTo(50),
query,
"https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png",
TimeSpan.MaxValue,
MusicPlatform.Radio,
query
);
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"));
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)
{
@@ -50,8 +40,8 @@ public class RadioResolver : IRadioResolver
{
return query;
}
if (query.Contains(".pls"))
{
//File1=http://armitunes.com:8000/
//Regex.Match(query)
try
@@ -65,14 +55,13 @@ public class RadioResolver : IRadioResolver
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
*/
# This is a comment
C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
*/
try
{
var m = m3uRegex.Match(file);
@@ -85,9 +74,7 @@ public class RadioResolver : IRadioResolver
return null;
}
}
if (query.Contains(".asx"))
{
//<ref href="http://armitunes.com:8000"/>
try
{
@@ -100,15 +87,14 @@ public class RadioResolver : IRadioResolver
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>
*/
<?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);
@@ -120,8 +106,7 @@ public class RadioResolver : IRadioResolver
Log.Warning($"Failed reading .xspf:\n{file}");
return null;
}
}
return query;
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
namespace NadekoBot.Modules.Music.Resolvers;
@@ -16,37 +17,29 @@ public sealed class SoundcloudResolver : ISoundcloudResolver
_httpFactory = httpFactory;
}
public bool IsSoundCloudLink(string url) =>
System.Text.RegularExpressions.Regex.IsMatch(url, "(.*)(soundcloud.com|snd.sc)(.*)");
public bool IsSoundCloudLink(string url)
=> 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;
}
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();
var cachableTracks = videosChunk.Select(VideoModelToCachedData).ToList();
await cachableTracks.Select(_trackCacher.CacheTrackDataAsync).WhenAll();
foreach(var info in cachableTracks.Select(CachableDataToTrackInfo))
{
yield return info;
}
foreach (var info in cachableTracks.Select(CachableDataToTrackInfo)) yield return info;
}
}
private ICachableTrackData VideoModelToCachedData(SoundCloudVideo svideo)
=> new CachableTrackData()
=> new CachableTrackData
{
Title = svideo.FullName,
Url = svideo.TrackLink,
@@ -55,16 +48,14 @@ public sealed class SoundcloudResolver : ISoundcloudResolver
Id = svideo.Id.ToString(),
Platform = MusicPlatform.SoundCloud
};
private ITrackInfo CachableDataToTrackInfo(ICachableTrackData trackData)
=> new SimpleTrackInfo(
trackData.Title,
=> new SimpleTrackInfo(trackData.Title,
trackData.Url,
trackData.Thumbnail,
trackData.Duration,
trackData.Platform,
GetStreamUrl(trackData.Id)
);
GetStreamUrl(trackData.Id));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private string GetStreamUrl(string trackId)
@@ -75,7 +66,7 @@ public sealed class SoundcloudResolver : ISoundcloudResolver
var cached = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.SoundCloud);
if (cached is not null)
return CachableDataToTrackInfo(cached);
var svideo = !IsSoundCloudLink(query)
? await _sc.GetVideoByQueryAsync(query)
: await _sc.ResolveVideoAsync(query);
@@ -85,7 +76,7 @@ public sealed class SoundcloudResolver : ISoundcloudResolver
var cachableData = VideoModelToCachedData(svideo);
await _trackCacher.CacheTrackDataByQueryAsync(query, cachableData);
return CachableDataToTrackInfo(cachableData);
}
}
}

View File

@@ -7,8 +7,11 @@ public sealed class TrackResolveProvider : ITrackResolveProvider
private readonly ISoundcloudResolver _soundcloudResolver;
private readonly IRadioResolver _radioResolver;
public TrackResolveProvider(IYoutubeResolver ytResolver, ILocalTrackResolver localResolver,
ISoundcloudResolver soundcloudResolver, IRadioResolver radioResolver)
public TrackResolveProvider(
IYoutubeResolver ytResolver,
ILocalTrackResolver localResolver,
ISoundcloudResolver soundcloudResolver,
IRadioResolver radioResolver)
{
_ytResolver = ytResolver;
_localResolver = localResolver;
@@ -45,13 +48,9 @@ public sealed class TrackResolveProvider : ITrackResolveProvider
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"));
}
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"));
}

View File

@@ -5,17 +5,22 @@ namespace NadekoBot.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"};
private static readonly string[] durationFormats =
{
"ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss"
};
public Regex YtVideoIdRegex { get; }
= new(
@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{6,11})",
RegexOptions.Compiled
);
private static readonly Regex expiryRegex = new(@"(?:[\?\&]expire\=(?<timestamp>\d+))");
private static readonly Regex _simplePlaylistRegex = new(@"&list=(?<id>[\w\-]{12,})", RegexOptions.Compiled);
public Regex YtVideoIdRegex { get; } =
new(@"(?: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;
@@ -27,72 +32,51 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
_trackCacher = trackCacher;
_google = google;
_ytdlPlaylistOperation =
new("-4 " +
"--geo-bypass " +
"--encoding UTF8 " +
"-f bestaudio " +
"-e " +
"--get-url " +
"--get-id " +
"--get-thumbnail " +
"--get-duration " +
"--no-check-certificate " +
"-i " +
"--yes-playlist " +
"-- \"{0}\"");
_ytdlPlaylistOperation = new("-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("-4 " +
"--geo-bypass " +
"--encoding UTF8 " +
"-f bestaudio " +
"-e " +
"--get-url " +
"--get-id " +
"--get-thumbnail " +
"--get-duration " +
"--no-check-certificate " +
"-- \"{0}\"");
_ytdlSearchOperation =
new("-4 " +
"--geo-bypass " +
"--encoding UTF8 " +
"-f bestaudio " +
"-e " +
"--get-url " +
"--get-id " +
"--get-thumbnail " +
"--get-duration " +
"--no-check-certificate " +
"--default-search " +
"\"ytsearch:\" -- \"{0}\"");
_ytdlIdOperation = new("-4 "
+ "--geo-bypass "
+ "--encoding UTF8 "
+ "-f bestaudio "
+ "-e "
+ "--get-url "
+ "--get-id "
+ "--get-thumbnail "
+ "--get-duration "
+ "--no-check-certificate "
+ "-- \"{0}\"");
_ytdlSearchOperation = new("-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)
@@ -102,26 +86,15 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
}
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;
var thumbnail = Uri.IsWellFormedUriString(dataArray[3], UriKind.Absolute) ? dataArray[3].Trim() : string.Empty;
return new(
dataArray[0],
dataArray[1],
thumbnail,
dataArray[2],
time
);
return new(dataArray[0], dataArray[1], thumbnail, dataArray[2], time);
}
private ITrackInfo DataToInfo(in YtTrackData trackData)
=> new RemoteTrackInfo(
trackData.Title,
=> new RemoteTrackInfo(trackData.Title,
$"https://youtube.com/watch?v={trackData.Id}",
trackData.Thumbnail,
trackData.Duration,
@@ -129,13 +102,10 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
CreateCacherFactory(trackData.Id));
private Func<Task<string?>> CreateCacherFactory(string id)
=> () => _trackCacher.GetOrCreateStreamLink(
id,
=> () => _trackCacher.GetOrCreateStreamLink(id,
MusicPlatform.Youtube,
async () => await ExtractNewStreamUrlAsync(id)
);
async () => await ExtractNewStreamUrlAsync(id));
private static readonly Regex expiryRegex = new(@"(?:[\?\&]expire\=(?<timestamp>\d+))");
private static TimeSpan GetExpiry(string streamUrl)
{
var match = expiryRegex.Match(streamUrl);
@@ -157,54 +127,41 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
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)
);
await Task.WhenAll(_trackCacher.CacheTrackDataAsync(toReturn.ToCachedData(id)),
CacheStreamUrlAsync(trackInfo));
return toReturn;
}
return DataToInfo(new(
cachedData.Title,
cachedData.Id,
cachedData.Thumbnail,
null,
cachedData.Duration
));
return DataToInfo(new(cachedData.Title, cachedData.Id, cachedData.Thumbnail, null, cachedData.Duration));
}
private Task CacheStreamUrlAsync(YtTrackData trackInfo)
=> _trackCacher.CacheStreamUrlAsync(
trackInfo.Id,
=> _trackCacher.CacheStreamUrlAsync(trackInfo.Id,
MusicPlatform.Youtube,
trackInfo.StreamUrl!,
GetExpiry(trackInfo.StreamUrl!)
);
private static readonly Regex _simplePlaylistRegex = new(@"&list=(?<id>[\w\-]{12,})", RegexOptions.Compiled);
GetExpiry(trackInfo.StreamUrl!));
public async IAsyncEnumerable<ITrackInfo> ResolveTracksByPlaylistIdAsync(string playlistId)
{
@@ -241,11 +198,9 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
continue;
var info = DataToInfo(in trackData);
await Task.WhenAll(
_trackCacher.CacheTrackDataAsync(info.ToCachedData(trackData.Id)),
CacheStreamUrlAsync(trackData)
);
await Task.WhenAll(_trackCacher.CacheTrackDataAsync(info.ToCachedData(trackData.Id)),
CacheStreamUrlAsync(trackData));
trackIds.Add(trackData.Id);
yield return info;
}
@@ -306,7 +261,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
public Task<ITrackInfo?> ResolveByQueryAsync(string query)
=> ResolveByQueryAsync(query, true);
public async Task<ITrackInfo?> ResolveByQueryAsync(string query, bool tryResolving)
{
if (tryResolving)
@@ -315,29 +270,44 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
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)
);
await Task.WhenAll(_trackCacher.CacheTrackDataByQueryAsync(query, trackInfo.ToCachedData(trackData.Id)),
CacheStreamUrlAsync(trackData));
return trackInfo;
}
return DataToInfo(new(
cachedData.Title,
cachedData.Id,
cachedData.Thumbnail,
null,
cachedData.Duration
));
return DataToInfo(new(cachedData.Title, cachedData.Id, cachedData.Thumbnail, null, cachedData.Duration));
}
}
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;
}
}
}

View File

@@ -1,14 +1,27 @@
#nullable disable
using NadekoBot.Services.Database.Models;
using NadekoBot.Modules.Administration.Services;
using NadekoBot.Modules.Music.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Music;
[NoPublicBot]
public sealed partial class Music : NadekoModule<IMusicService>
{
public enum All { All = -1 }
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
}
public const string MusicIconUrl = "http://i.imgur.com/nhKS3PT.png";
private const int LQ_ITEMS_PER_PAGE = 9;
private static readonly SemaphoreSlim voiceChannelLock = new(1, 1);
private readonly ILogCommandService _logService;
public Music(ILogCommandService _logService)
@@ -16,9 +29,9 @@ public sealed partial class Music : NadekoModule<IMusicService>
private async Task<bool> ValidateAsync()
{
var user = (IGuildUser) ctx.User;
var user = (IGuildUser)ctx.User;
var userVoiceChannelId = user.VoiceChannel?.Id;
if (userVoiceChannelId is null)
{
await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
@@ -35,7 +48,6 @@ public sealed partial class Music : NadekoModule<IMusicService>
return true;
}
private static readonly SemaphoreSlim voiceChannelLock = new(1, 1);
private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null)
{
botUser ??= await ctx.Guild.GetCurrentUserAsync();
@@ -50,12 +62,12 @@ public sealed partial class Music : NadekoModule<IMusicService>
voiceChannelLock.Release();
}
}
private async Task<bool> QueuePreconditionInternalAsync()
{
var user = (IGuildUser) ctx.User;
var user = (IGuildUser)ctx.User;
var voiceChannelId = user.VoiceChannel?.Id;
if (voiceChannelId is null)
{
await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
@@ -63,10 +75,10 @@ public sealed partial class Music : NadekoModule<IMusicService>
}
_ = ctx.Channel.TriggerTypingAsync();
var botUser = await ctx.Guild.GetCurrentUserAsync();
await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
if (botUser.VoiceChannel?.Id != voiceChannelId)
{
await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
@@ -81,18 +93,15 @@ public sealed partial class Music : NadekoModule<IMusicService>
var succ = await QueuePreconditionInternalAsync();
if (!succ)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel);
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
var (trackInfo, index) = await mp.TryEnqueueTrackAsync(query,
ctx.User.ToString(),
asNext,
forcePlatform);
var (trackInfo, index) = await mp.TryEnqueueTrackAsync(query, ctx.User.ToString(), asNext, forcePlatform);
if (trackInfo is null)
{
await ReplyErrorLocalizedAsync(strs.song_not_found);
@@ -102,10 +111,10 @@ public sealed partial class Music : NadekoModule<IMusicService>
try
{
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(GetText(strs.queued_song) + " #" + (index + 1), MusicIconUrl)
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
.WithFooter(trackInfo.Platform.ToString());
.WithOkColor()
.WithAuthor(GetText(strs.queued_song) + " #" + (index + 1), MusicIconUrl)
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
.WithFooter(trackInfo.Platform.ToString());
if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail))
embed.WithThumbnailUrl(trackInfo.Thumbnail);
@@ -128,12 +137,12 @@ public sealed partial class Music : NadekoModule<IMusicService>
{
if (--index < 0)
return;
var succ = await QueuePreconditionInternalAsync();
if (!succ)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel);
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
@@ -142,13 +151,14 @@ public sealed partial class Music : NadekoModule<IMusicService>
mp.MoveTo(index);
}
// join vc
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Join()
{
var user = (IGuildUser) ctx.User;
var user = (IGuildUser)ctx.User;
var voiceChannelId = user.VoiceChannel?.Id;
@@ -162,7 +172,8 @@ public sealed partial class Music : NadekoModule<IMusicService>
}
// leave vc (destroy)
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Destroy()
{
@@ -172,39 +183,45 @@ public sealed partial class Music : NadekoModule<IMusicService>
await _service.LeaveVoiceChannelAsync(ctx.Guild.Id);
}
// play - no args = next
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[Priority(2)]
public Task Play()
=> Next();
// play - index = skip to that index
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task Play(int index)
=> MoveToIndex(index);
// play - query = q(query)
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public Task Play([Leftover] string query)
=> QueueByQuery(query);
[NadekoCommand, Aliases]
=> QueueByQuery(query);
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public Task Queue([Leftover] string query)
=> QueueByQuery(query);
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public Task QueueNext([Leftover] string query)
=> QueueByQuery(query, asNext: true);
=> QueueByQuery(query, true);
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Volume(int vol)
{
@@ -213,7 +230,7 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyErrorLocalizedAsync(strs.volume_input_invalid);
return;
}
var valid = await ValidateAsync();
if (!valid)
return;
@@ -222,7 +239,8 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyConfirmLocalizedAsync(strs.volume_set(vol));
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Next()
{
@@ -231,17 +249,12 @@ public sealed partial class Music : NadekoModule<IMusicService>
return;
var success = await _service.PlayAsync(ctx.Guild.Id, ((IGuildUser)ctx.User).VoiceChannel.Id);
if (!success)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
if (!success) await ReplyErrorLocalizedAsync(strs.no_player);
}
private const int LQ_ITEMS_PER_PAGE = 9;
// list queue, relevant page
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task ListQueue()
{
@@ -251,12 +264,13 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
await ListQueue((mp.CurrentIndex / LQ_ITEMS_PER_PAGE) + 1);
}
// list queue, specify page
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task ListQueue(int page)
{
@@ -269,15 +283,12 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
IEmbedBuilder printAction(int curPage)
{
var desc = string.Empty;
var current = mp.GetCurrentTrack(out var currentIndex);
if (current is not null)
{
desc = $"`🔊` {current.PrettyFullName()}\n\n" + desc;
}
if (current is not null) desc = $"`🔊` {current.PrettyFullName()}\n\n" + desc;
var repeatType = mp.Repeat;
var add = string.Empty;
@@ -301,42 +312,37 @@ public sealed partial class Music : NadekoModule<IMusicService>
}
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()}";
})
.Join('\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()}";
})
.Join('\n');
if (!string.IsNullOrWhiteSpace(add))
desc = add + "\n" + desc;
var embed = _eb.Create()
.WithAuthor(GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
MusicIconUrl)
.WithDescription(desc)
.WithFooter($" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
.WithOkColor();
.WithAuthor(GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
MusicIconUrl)
.WithDescription(desc)
.WithFooter($" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
.WithOkColor();
return embed;
}
await ctx.SendPaginatedConfirmAsync(
page,
printAction,
tracks.Count,
LQ_ITEMS_PER_PAGE,
false);
await ctx.SendPaginatedConfirmAsync(page, printAction, tracks.Count, LQ_ITEMS_PER_PAGE, false);
}
// search
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task QueueSearch([Leftover] string query)
{
@@ -350,19 +356,14 @@ public sealed partial class Music : NadekoModule<IMusicService>
return;
}
var resultsString = videos
.Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Title)}\n\t{x.Url}")
.Join('\n');
var resultsString = videos.Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Title)}\n\t{x.Url}").Join('\n');
var msg = await SendConfirmAsync(resultsString);
try
{
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
if (input is null
|| !int.TryParse(input, out var index)
|| (index -= 1) < 0
|| index >= videos.Count)
if (input is null || !int.TryParse(input, out var index) || (index -= 1) < 0 || index >= videos.Count)
{
_logService.AddDeleteIgnore(msg.Id);
try
@@ -375,6 +376,7 @@ public sealed partial class Music : NadekoModule<IMusicService>
return;
}
query = videos[index].Url;
await Play(query);
@@ -392,7 +394,8 @@ public sealed partial class Music : NadekoModule<IMusicService>
}
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task TrackRemove(int index)
@@ -402,7 +405,7 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyErrorLocalizedAsync(strs.removed_song_error);
return;
}
var valid = await ValidateAsync();
if (!valid)
return;
@@ -412,24 +415,24 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
if (!mp.TryRemoveTrackAt(index - 1, out var song))
{
await ReplyErrorLocalizedAsync(strs.removed_song_error);
return;
}
var embed = _eb.Create()
.WithAuthor(GetText(strs.removed_song) + " #" + index, MusicIconUrl)
.WithDescription(song.PrettyName())
.WithFooter(song.PrettyInfo())
.WithErrorColor();
.WithAuthor(GetText(strs.removed_song) + " #" + index, MusicIconUrl)
.WithDescription(song.PrettyName())
.WithFooter(song.PrettyInfo())
.WithErrorColor();
await _service.SendToOutputAsync(ctx.Guild.Id, embed);
}
public enum All { All = -1 }
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task TrackRemove(All _ = All.All)
@@ -443,12 +446,13 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
mp.Clear();
await ReplyConfirmLocalizedAsync(strs.queue_cleared);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Stop()
{
@@ -461,33 +465,28 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyErrorLocalizedAsync(strs.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
};
private PlayerRepeatType InputToDbType(InputRepeatType type) => type switch
{
InputRepeatType.None => PlayerRepeatType.None,
InputRepeatType.Queue => PlayerRepeatType.Queue,
InputRepeatType.Track => PlayerRepeatType.Track,
_ => PlayerRepeatType.Queue
};
[NadekoCommand, Aliases]
[NadekoCommand]
[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)
@@ -497,8 +496,9 @@ public sealed partial class Music : NadekoModule<IMusicService>
else
await ReplyConfirmLocalizedAsync(strs.repeating_track);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Pause()
{
@@ -514,19 +514,22 @@ public sealed partial class Music : NadekoModule<IMusicService>
mp.TogglePause();
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public Task Radio(string radioLink)
=> QueueByQuery(radioLink, false, MusicPlatform.Radio);
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public Task Local([Leftover] string path)
=> QueueByQuery(path, false, MusicPlatform.Local);
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task LocalPlaylist([Leftover] string dirPath)
@@ -534,9 +537,9 @@ public sealed partial class Music : NadekoModule<IMusicService>
if (string.IsNullOrWhiteSpace(dirPath))
return;
var user = (IGuildUser) ctx.User;
var user = (IGuildUser)ctx.User;
var voiceChannelId = user.VoiceChannel?.Id;
if (voiceChannelId is null)
{
await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
@@ -544,29 +547,30 @@ public sealed partial class Music : NadekoModule<IMusicService>
}
_ = ctx.Channel.TriggerTypingAsync();
var botUser = await ctx.Guild.GetCurrentUserAsync();
await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
if (botUser.VoiceChannel?.Id != voiceChannelId)
{
await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
return;
}
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel);
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
await _service.EnqueueDirectoryAsync(mp, dirPath, ctx.User.ToString());
await ReplyConfirmLocalizedAsync(strs.dir_queue_complete);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task MoveSong(int from, int to)
{
@@ -579,8 +583,8 @@ public sealed partial class Music : NadekoModule<IMusicService>
var valid = await ValidateAsync();
if (!valid)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel);
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
@@ -593,13 +597,13 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyErrorLocalizedAsync(strs.invalid_input);
return;
}
var embed = _eb.Create()
.WithTitle(track.Title.TrimTo(65))
.WithAuthor(GetText(strs.song_moved), MusicIconUrl)
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
.WithOkColor();
.WithTitle(track.Title.TrimTo(65))
.WithAuthor(GetText(strs.song_moved), MusicIconUrl)
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
.WithOkColor();
if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
embed.WithUrl(track.Url);
@@ -607,12 +611,14 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ctx.Channel.EmbedAsync(embed);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public Task SoundCloudQueue([Leftover] string query)
=> QueueByQuery(query, false, MusicPlatform.SoundCloud);
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task SoundCloudPl([Leftover] string playlist)
{
@@ -623,13 +629,13 @@ public sealed partial class Music : NadekoModule<IMusicService>
if (!succ)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel);
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
_ = ctx.Channel.TriggerTypingAsync();
await _service.EnqueueSoundcloudPlaylistAsync(mp, playlist, ctx.User.ToString());
@@ -637,7 +643,8 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ctx.OkAsync();
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Playlist([Leftover] string playlistQuery)
{
@@ -648,7 +655,7 @@ public sealed partial class Music : NadekoModule<IMusicService>
if (!succ)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel);
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
@@ -664,14 +671,16 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyErrorLocalizedAsync(strs.no_search_results);
return;
}
await ctx.OkAsync();
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task NowPlaying()
{
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel);
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
@@ -682,35 +691,39 @@ public sealed partial class Music : NadekoModule<IMusicService>
if (currentTrack is null)
return;
var embed = _eb.Create().WithOkColor()
.WithAuthor(GetText(strs.now_playing), MusicIconUrl)
.WithDescription(currentTrack.PrettyName())
.WithThumbnailUrl(currentTrack.Thumbnail)
.WithFooter($"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(GetText(strs.now_playing), MusicIconUrl)
.WithDescription(currentTrack.PrettyName())
.WithThumbnailUrl(currentTrack.Thumbnail)
.WithFooter(
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
await ctx.Channel.EmbedAsync(embed);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task PlaylistShuffle()
{
var valid = await ValidateAsync();
if (!valid)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel);
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
mp.ShuffleQueue();
await ReplyConfirmLocalizedAsync(strs.queue_shuffled);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task SetMusicChannel()
@@ -719,8 +732,9 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyConfirmLocalizedAsync(strs.set_music_channel);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task UnsetMusicChannel()
@@ -730,19 +744,21 @@ public sealed partial class Music : NadekoModule<IMusicService>
await ReplyConfirmLocalizedAsync(strs.unset_music_channel);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task AutoDisconnect()
{
var newState = await _service.ToggleAutoDisconnectAsync(ctx.Guild.Id);
if(newState)
if (newState)
await ReplyConfirmLocalizedAsync(strs.autodc_enable);
else
await ReplyConfirmLocalizedAsync(strs.autodc_disable);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task MusicQuality()
@@ -750,8 +766,9 @@ public sealed partial class Music : NadekoModule<IMusicService>
var quality = await _service.GetMusicQualityAsync(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.current_music_quality(Format.Bold(quality.ToString())));
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task MusicQuality(QualityPreset preset)
@@ -759,4 +776,4 @@ public sealed partial class Music : NadekoModule<IMusicService>
await _service.SetMusicQualityAsync(ctx.Guild.Id, preset);
await ReplyConfirmLocalizedAsync(strs.music_quality_set(Format.Bold(preset.ToString())));
}
}
}

View File

@@ -1,7 +1,7 @@
#nullable disable
using NadekoBot.Services.Database.Models;
using NadekoBot.Db;
using NadekoBot.Modules.Music.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Music;
@@ -10,6 +10,7 @@ public sealed partial class Music
[Group]
public sealed class PlaylistCommands : NadekoModule<IMusicService>
{
private static readonly SemaphoreSlim _playlistLock = new(1, 1);
private readonly DbService _db;
private readonly IBotCredentials _creds;
@@ -18,7 +19,7 @@ public sealed partial class Music
_db = db;
_creds = creds;
}
private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null)
{
botUser ??= await ctx.Guild.GetCurrentUserAsync();
@@ -34,7 +35,8 @@ public sealed partial class Music
}
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Playlists([Leftover] int num = 1)
{
@@ -48,17 +50,17 @@ public sealed partial class Music
playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num);
}
var embed = _eb
.Create(ctx)
.WithAuthor(GetText(strs.playlists_page(num)), MusicIconUrl)
.WithDescription(string.Join("\n", playlists.Select(r =>
GetText(strs.playlists(r.Id, r.Name, r.Author, r.Songs.Count)))))
.WithOkColor();
var embed = _eb.Create(ctx)
.WithAuthor(GetText(strs.playlists_page(num)), MusicIconUrl)
.WithDescription(string.Join("\n",
playlists.Select(r => GetText(strs.playlists(r.Id, r.Name, r.Author, r.Songs.Count)))))
.WithOkColor();
await ctx.Channel.EmbedAsync(embed);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task DeletePlaylist([Leftover] int id)
{
@@ -69,14 +71,12 @@ public sealed partial class Music
var pl = uow.MusicPlaylists.FirstOrDefault(x => x.Id == 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)
{
@@ -89,7 +89,8 @@ public sealed partial class Music
await ReplyConfirmLocalizedAsync(strs.playlist_deleted);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task PlaylistShow(int id, int page = 1)
{
@@ -102,21 +103,22 @@ public sealed partial class Music
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 _eb.Create()
.WithTitle($"\"{mpl.Name}\" by {mpl.Author}")
.WithOkColor()
.WithDescription(str);
}, mpl.Songs.Count, 20);
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 _eb.Create().WithTitle($"\"{mpl.Name}\" by {mpl.Author}").WithOkColor().WithDescription(str);
},
mpl.Songs.Count,
20);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Save([Leftover] string name)
{
@@ -127,49 +129,45 @@ public sealed partial class Music
}
var songs = mp.GetQueuedTracks()
.Select(s => new PlaylistSong()
{
Provider = s.Platform.ToString(),
ProviderType = (MusicType)s.Platform,
Title = s.Title,
Query = s.Platform == MusicPlatform.Local ? s.GetStreamUrl().Result!.Trim('"') : s.Url,
}).ToList();
.Select(s => new PlaylistSong
{
Provider = s.Platform.ToString(),
ProviderType = (MusicType)s.Platform,
Title = s.Title,
Query = s.Platform == MusicPlatform.Local ? s.GetStreamUrl().Result!.Trim('"') : s.Url
})
.ToList();
MusicPlaylist playlist;
await using (var uow = _db.GetDbContext())
{
playlist = new()
{
Name = name,
Author = ctx.User.Username,
AuthorId = ctx.User.Id,
Songs = songs.ToList(),
Name = name, Author = ctx.User.Username, AuthorId = ctx.User.Id, Songs = songs.ToList()
};
uow.MusicPlaylists.Add(playlist);
await uow.SaveChangesAsync();
}
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.playlist_saved))
.AddField(GetText(strs.name), name)
.AddField(GetText(strs.id), playlist.Id.ToString()));
.WithOkColor()
.WithTitle(GetText(strs.playlist_saved))
.AddField(GetText(strs.name), name)
.AddField(GetText(strs.id), playlist.Id.ToString()));
}
private static readonly SemaphoreSlim _playlistLock = new(1, 1);
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Load([Leftover] int id)
{
// expensive action, 1 at a time
await _playlistLock.WaitAsync();
try
{
var user = (IGuildUser) ctx.User;
{
var user = (IGuildUser)ctx.User;
var voiceChannelId = user.VoiceChannel?.Id;
if (voiceChannelId is null)
{
await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
@@ -177,23 +175,23 @@ public sealed partial class Music
}
_ = ctx.Channel.TriggerTypingAsync();
var botUser = await ctx.Guild.GetCurrentUserAsync();
await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
if (botUser.VoiceChannel?.Id != voiceChannelId)
{
await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
return;
}
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel);
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
MusicPlaylist mpl;
await using (var uow = _db.GetDbContext())
{
@@ -209,22 +207,17 @@ public sealed partial class Music
IUserMessage msg = null;
try
{
msg = await ctx.Channel
.SendMessageAsync(GetText(strs.attempting_to_queue(Format.Bold(mpl.Songs.Count.ToString()))));
msg = await ctx.Channel.SendMessageAsync(
GetText(strs.attempting_to_queue(Format.Bold(mpl.Songs.Count.ToString()))));
}
catch (Exception)
{
}
await mp.EnqueueManyAsync(
mpl.Songs.Select(x => (x.Query, (MusicPlatform) x.ProviderType)),
ctx.User.ToString()
);
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(strs.playlist_queue_complete));
}
if (msg != null) await msg.ModifyAsync(m => m.Content = GetText(strs.playlist_queue_complete));
}
finally
{
@@ -232,4 +225,4 @@ public sealed partial class Music
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
#nullable disable
using System.Reflection;
using Ayu.Discord.Voice;
using System.Reflection;
namespace NadekoBot.Modules.Music.Services;
@@ -8,10 +8,10 @@ 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();
private readonly ConcurrentDictionary<ulong, SemaphoreSlim> _voiceGatewayLocks = new();
private readonly DiscordSocketClient _client;
private readonly MethodInfo _sendVoiceStateUpdateMethodInfo;
private readonly object _dnetApiClient;
@@ -23,13 +23,17 @@ public sealed class AyuVoiceStateService : INService
_currentUserId = _client.CurrentUser.Id;
var prop = _client.GetType()
.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
.First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient");
.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
.First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient");
_dnetApiClient = prop.GetValue(_client, null);
_sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType()
.GetMethod("SendVoiceStateUpdateAsync",
types: new[] { typeof(ulong), typeof(ulong?), typeof(bool), typeof(bool), typeof(RequestOptions) });
.GetMethod("SendVoiceStateUpdateAsync",
new[]
{
typeof(ulong), typeof(ulong?), typeof(bool),
typeof(bool), typeof(RequestOptions)
});
_client.LeftGuild += ClientOnLeftGuild;
}
@@ -44,29 +48,32 @@ public sealed class AyuVoiceStateService : INService
return Task.CompletedTask;
}
private Task InvokeSendVoiceStateUpdateAsync(ulong guildId, ulong? channelId = null, bool isDeafened = false, bool isMuted = false)
private Task InvokeSendVoiceStateUpdateAsync(
ulong guildId,
ulong? channelId = null,
bool isDeafened = false,
bool isMuted = false)
// return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted);
=> (Task) _sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient, new object[] {guildId, channelId, isMuted, isDeafened, null});
=> (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 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)
{
if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId && newState.VoiceChannel?.Id is null)
complete.TrySetResult(true);
}
return Task.CompletedTask;
}
@@ -89,6 +96,7 @@ public sealed class AyuVoiceStateService : INService
_client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
}
}
public async Task LeaveVoiceChannel(ulong guildId)
{
var gwLock = GetVoiceGatewayLock(guildId);
@@ -105,8 +113,10 @@ public sealed class AyuVoiceStateService : INService
private async Task<IVoiceProxy> InternalConnectToVcAsync(ulong guildId, ulong channelId)
{
var voiceStateUpdatedSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var voiceServerUpdatedSource = new TaskCompletionSource<SocketVoiceServer>(TaskCreationOptions.RunContinuationsAsynchronously);
var voiceStateUpdatedSource =
new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var voiceServerUpdatedSource =
new TaskCompletionSource<SocketVoiceServer>(TaskCreationOptions.RunContinuationsAsynchronously);
Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
{
@@ -123,14 +133,11 @@ public sealed class AyuVoiceStateService : INService
Task OnVoiceServerUpdated(SocketVoiceServer data)
{
if (data.Guild.Id == guildId)
{
voiceServerUpdatedSource.TrySetResult(data);
}
if (data.Guild.Id == guildId) voiceServerUpdatedSource.TrySetResult(data);
return Task.CompletedTask;
}
try
{
_client.VoiceServerUpdated += OnVoiceServerUpdated;
@@ -149,10 +156,8 @@ public sealed class AyuVoiceStateService : INService
// 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
@@ -164,26 +169,20 @@ public sealed class AyuVoiceStateService : INService
return null;
var voiceServerData = await voiceServerUpdatedSource.Task;
VoiceGateway CreateVoiceGatewayLocal() =>
new(
guildId,
_currentUserId,
session,
voiceServerData.Token,
voiceServerData.Endpoint
);
var current = _voiceProxies.AddOrUpdate(
guildId,
VoiceGateway CreateVoiceGatewayLocal()
{
return new(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;
@@ -212,4 +211,4 @@ public sealed class AyuVoiceStateService : INService
public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy)
=> _voiceProxies.TryGetValue(guildId, out proxy);
}
}

View File

@@ -1,23 +1,23 @@
using System.Diagnostics.CodeAnalysis;
using NadekoBot.Services.Database.Models;
using NadekoBot.Services.Database.Models;
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Modules.Music.Services;
public interface IMusicService : IPlaceholderProvider
{
/// <summary>
/// Leave voice channel in the specified guild if it's connected to one
/// 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
/// 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, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer);
Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string playlistId, string queuer);
@@ -32,4 +32,4 @@ public interface IMusicService : IPlaceholderProvider
Task<bool> ToggleAutoDisconnectAsync(ulong guildId);
Task<QualityPreset> GetMusicQualityAsync(ulong guildId);
Task SetMusicQualityAsync(ulong guildId, QualityPreset preset);
}
}

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using NadekoBot.Db;
using NadekoBot.Services.Database.Models;
using NadekoBot.Db;
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Modules.Music.Services;
@@ -22,9 +22,17 @@ public sealed class MusicService : IMusicService
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,
public MusicService(
AyuVoiceStateService voiceStateService,
ITrackResolveProvider trackResolveProvider,
DbService db,
IYoutubeResolver ytResolver,
ILocalTrackResolver localResolver,
ISoundcloudResolver scResolver,
DiscordSocketClient client,
IBotStrings strings,
IGoogleApiService googleApiService,
YtLoader ytLoader,
IEmbedBuilderService eb)
{
_voiceStateService = voiceStateService;
@@ -42,10 +50,10 @@ public sealed class MusicService : IMusicService
_players = new();
_outputChannels = new ConcurrentDictionary<ulong, (ITextChannel, ITextChannel?)>();
_settings = new();
_client.LeftGuild += ClientOnLeftGuild;
}
private void DisposeMusicPlayer(IMusicPlayer musicPlayer)
{
musicPlayer.Kill();
@@ -55,10 +63,7 @@ public sealed class MusicService : IMusicService
private void RemoveMusicPlayer(ulong guildId)
{
_outputChannels.TryRemove(guildId, out _);
if (_players.TryRemove(guildId, out var mp))
{
DisposeMusicPlayer(mp);
}
if (_players.TryRemove(guildId, out var mp)) DisposeMusicPlayer(mp);
}
private Task ClientOnLeftGuild(SocketGuild guild)
@@ -73,7 +78,7 @@ public sealed class MusicService : IMusicService
await _voiceStateService.LeaveVoiceChannel(guildId);
}
public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId)
public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId)
=> _voiceStateService.JoinVoiceChannel(guildId, voiceChannelId);
public async Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel)
@@ -81,7 +86,7 @@ public sealed class MusicService : IMusicService
var newPLayer = await CreateMusicPlayerInternalAsync(contextChannel.GuildId, contextChannel);
if (newPLayer is null)
return null;
return _players.GetOrAdd(contextChannel.GuildId, newPLayer);
}
@@ -109,7 +114,7 @@ public sealed class MusicService : IMusicService
{
if (mp.IsKilled)
break;
mp.EnqueueTrack(track, queuer);
}
}
@@ -121,7 +126,7 @@ public sealed class MusicService : IMusicService
{
if (mp.IsKilled)
break;
mp.EnqueueTrack(track, queuer);
++i;
}
@@ -134,10 +139,7 @@ public sealed class MusicService : IMusicService
var queue = new MusicQueue();
var resolver = _trackResolveProvider;
if (!_voiceStateService.TryGetProxy(guildId, out var proxy))
{
return null;
}
if (!_voiceStateService.TryGetProxy(guildId, out var proxy)) return null;
var settings = await GetSettingsInternalAsync(guildId);
@@ -147,30 +149,19 @@ public sealed class MusicService : IMusicService
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
);
var mp = new MusicPlayer(queue, resolver, proxy, settings.QualityPreset);
mp.SetRepeat(settings.PlayerRepeat);
if (settings.Volume is >= 0 and <= 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);
@@ -197,10 +188,10 @@ public sealed class MusicService : IMusicService
{
_ = lastFinishedMessage?.DeleteAsync();
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(GetText(guildId, strs.finished_song), Music.MusicIconUrl)
.WithDescription(trackInfo.PrettyName())
.WithFooter(trackInfo.PrettyTotalTime());
.WithOkColor()
.WithAuthor(GetText(guildId, strs.finished_song), Music.MusicIconUrl)
.WithDescription(trackInfo.PrettyName())
.WithFooter(trackInfo.PrettyTotalTime());
lastFinishedMessage = await SendToOutputAsync(guildId, embed);
};
@@ -212,10 +203,11 @@ public sealed class MusicService : IMusicService
return async (mp, trackInfo, index) =>
{
_ = lastPlayingMessage?.DeleteAsync();
var embed = _eb.Create().WithOkColor()
.WithAuthor(GetText(guildId, strs.playing_song(index + 1)), Music.MusicIconUrl)
.WithDescription(trackInfo.PrettyName())
.WithFooter($"{mp.PrettyVolume()} | {trackInfo.PrettyInfo()}");
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(GetText(guildId, strs.playing_song(index + 1)), Music.MusicIconUrl)
.WithDescription(trackInfo.PrettyName())
.WithFooter($"{mp.PrettyVolume()} | {trackInfo.PrettyInfo()}");
lastPlayingMessage = await SendToOutputAsync(guildId, embed);
};
@@ -225,12 +217,8 @@ public sealed class MusicService : IMusicService
=> mp =>
{
if (_settings.TryGetValue(guildId, out var settings))
{
if (settings.AutoDisconnect)
{
return LeaveVoiceChannelAsync(guildId);
}
}
return Task.CompletedTask;
};
@@ -238,19 +226,12 @@ public sealed class MusicService : IMusicService
// 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 (!TryGetMusicPlayer(guildId, out var mp)) return false;
if (mp.IsStopped)
{
if (!_voiceStateService.TryGetProxy(guildId, out var proxy)
if (!_voiceStateService.TryGetProxy(guildId, out var proxy)
|| proxy.State == VoiceProxy.VoiceProxyState.Stopped)
{
await JoinVoiceChannelAsync(guildId, voiceChannelId);
}
}
mp.Next();
return true;
@@ -261,22 +242,19 @@ public sealed class MusicService : IMusicService
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;
}
if (videos.Count > 0) return videos;
}
catch (Exception ex)
{
@@ -289,13 +267,14 @@ public sealed class MusicService : IMusicService
}
catch (Exception ex)
{
Log.Warning("Failed getting video results with Google Api. " +
"Probably google api key missing: {ErrorMessage}", ex.Message);
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, LocStr str)
=> _strings.GetText(str, guildId);
@@ -304,11 +283,10 @@ public sealed class MusicService : IMusicService
// random song that's playing
yield return ("%music.playing%", () =>
{
var randomPlayingTrack = _players
.Select(x => x.Value.GetCurrentTrack(out _))
.Where(x => x is not null)
.Shuffle()
.FirstOrDefault();
var randomPlayingTrack = _players.Select(x => x.Value.GetCurrentTrack(out _))
.Where(x => x is not null)
.Shuffle()
.FirstOrDefault();
if (randomPlayingTrack is null)
return "-";
@@ -319,17 +297,14 @@ public sealed class MusicService : IMusicService
// number of servers currently listening to music
yield return ("%music.servers%", () =>
{
var count = _players
.Select(x => x.Value.GetCurrentTrack(out _))
.Count(x => x is not null);
var count = _players.Select(x => x.Value.GetCurrentTrack(out _)).Count(x => x is not null);
return count.ToString();
});
yield return ("%music.queued%", () =>
{
var count = _players
.Sum(x => x.Value.GetQueuedTracks().Count);
var count = _players.Sum(x => x.Value.GetQueuedTracks().Count);
return count.ToString();
});
@@ -348,7 +323,7 @@ public sealed class MusicService : IMusicService
return toReturn;
}
private async Task ModifySettingsInternalAsync<TState>(
ulong guildId,
Action<MusicPlayerSettings, TState> action,
@@ -360,7 +335,7 @@ public sealed class MusicService : IMusicService
await uow.SaveChangesAsync();
_settings[guildId] = ms;
}
public async Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId)
{
if (channelId is null)
@@ -368,29 +343,31 @@ public sealed class MusicService : IMusicService
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);
await ModifySettingsInternalAsync(guildId,
(settings, chId) =>
{
settings.MusicChannelId = chId;
},
channelId);
_outputChannels.AddOrUpdate(guildId, (channel, channel), (key, old) => (old.Default, channel));
_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);
await ModifySettingsInternalAsync(guildId,
(settings, _) =>
{
settings.MusicChannelId = null;
},
(ulong?)null);
if (_outputChannels.TryGetValue(guildId, out var old))
_outputChannels[guildId] = (old.Default, null);
@@ -398,10 +375,12 @@ public sealed class MusicService : IMusicService
public async Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType)
{
await ModifySettingsInternalAsync(guildId, (settings, type) =>
{
settings.PlayerRepeat = type;
}, repeatType);
await ModifySettingsInternalAsync(guildId,
(settings, type) =>
{
settings.PlayerRepeat = type;
},
repeatType);
if (TryGetMusicPlayer(guildId, out var mp))
mp.SetRepeat(repeatType);
@@ -411,12 +390,14 @@ public sealed class MusicService : IMusicService
{
if (value is < 0 or > 100)
throw new ArgumentOutOfRangeException(nameof(value));
await ModifySettingsInternalAsync(guildId, (settings, newValue) =>
{
settings.Volume = newValue;
}, value);
await ModifySettingsInternalAsync(guildId,
(settings, newValue) =>
{
settings.Volume = newValue;
},
value);
if (TryGetMusicPlayer(guildId, out var mp))
mp.SetVolume(value);
}
@@ -424,10 +405,12 @@ public sealed class MusicService : IMusicService
public async Task<bool> ToggleAutoDisconnectAsync(ulong guildId)
{
var newState = false;
await ModifySettingsInternalAsync(guildId, (settings, _) =>
{
newState = settings.AutoDisconnect = !settings.AutoDisconnect;
}, default(object));
await ModifySettingsInternalAsync(guildId,
(settings, _) =>
{
newState = settings.AutoDisconnect = !settings.AutoDisconnect;
},
default(object));
return newState;
}
@@ -440,10 +423,12 @@ public sealed class MusicService : IMusicService
}
public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset)
=> ModifySettingsInternalAsync(guildId, (settings, _) =>
{
settings.QualityPreset = preset;
}, preset);
=> ModifySettingsInternalAsync(guildId,
(settings, _) =>
{
settings.QualityPreset = preset;
},
preset);
#endregion
}
}

View File

@@ -3,7 +3,6 @@ namespace NadekoBot.Modules.Music.Services;
public sealed partial class YtLoader
{
public class InitRange
{
public string Start { get; set; }
@@ -69,4 +68,4 @@ public sealed partial class YtLoader
_videoId = videoId;
}
}
}
}

View File

@@ -7,15 +7,16 @@ 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[]
private static readonly string[] durationFormats =
{
@"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss", @"hhh\:mm\:ss"
};
private readonly IHttpClientFactory _httpFactory;
public YtLoader(IHttpClientFactory httpFactory)
=> _httpFactory = httpFactory;
@@ -54,7 +55,7 @@ public sealed partial class YtLoader
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;");
@@ -76,20 +77,19 @@ public sealed partial class YtLoader
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();
.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))
if (!track.TryGetProperty("videoRenderer", out var elem))
continue;
var videoId = elem.GetProperty("videoId").GetString();
@@ -97,18 +97,20 @@ public sealed partial class YtLoader
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,
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;
}
@@ -120,11 +122,9 @@ public sealed partial class YtLoader
return null; // todo future try selecting html
startIndex += YT_RESULT_INITIAL_DATA.Length;
var endIndex = 140_000 + startIndex + responseSpan[(startIndex + 20_000)..].IndexOf(YT_RESULT_JSON_END) + 20_000;
var endIndex =
140_000 + startIndex + responseSpan[(startIndex + 20_000)..].IndexOf(YT_RESULT_JSON_END) + 20_000;
startIndex += 140_000;
return response.AsMemory(
startIndex,
endIndex - startIndex
);
return response.AsMemory(startIndex, endIndex - startIndex);
}
}
}