Global usings and file scoped namespaces

This commit is contained in:
Kwoth
2021-12-19 05:14:11 +01:00
parent bc31dae965
commit ee33313519
548 changed files with 47528 additions and 49115 deletions

View File

@@ -1,5 +1,4 @@
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -7,215 +6,214 @@ using Ayu.Discord.Voice;
using Discord.WebSocket;
using NadekoBot.Services;
namespace NadekoBot.Modules.Music.Services
namespace NadekoBot.Modules.Music.Services;
public sealed class AyuVoiceStateService : INService
{
public sealed class AyuVoiceStateService : INService
// public delegate Task VoiceProxyUpdatedDelegate(ulong guildId, IVoiceProxy proxy);
// public event VoiceProxyUpdatedDelegate OnVoiceProxyUpdate = delegate { return Task.CompletedTask; };
private readonly ConcurrentDictionary<ulong, IVoiceProxy> _voiceProxies = new ConcurrentDictionary<ulong, IVoiceProxy>();
private readonly ConcurrentDictionary<ulong, SemaphoreSlim> _voiceGatewayLocks = new ConcurrentDictionary<ulong, SemaphoreSlim>();
private readonly DiscordSocketClient _client;
private readonly MethodInfo _sendVoiceStateUpdateMethodInfo;
private readonly object _dnetApiClient;
private readonly ulong _currentUserId;
public AyuVoiceStateService(DiscordSocketClient client)
{
// public delegate Task VoiceProxyUpdatedDelegate(ulong guildId, IVoiceProxy proxy);
// public event VoiceProxyUpdatedDelegate OnVoiceProxyUpdate = delegate { return Task.CompletedTask; };
private readonly ConcurrentDictionary<ulong, IVoiceProxy> _voiceProxies = new ConcurrentDictionary<ulong, IVoiceProxy>();
private readonly ConcurrentDictionary<ulong, SemaphoreSlim> _voiceGatewayLocks = new ConcurrentDictionary<ulong, SemaphoreSlim>();
private readonly DiscordSocketClient _client;
private readonly MethodInfo _sendVoiceStateUpdateMethodInfo;
private readonly object _dnetApiClient;
private readonly ulong _currentUserId;
_client = client;
_currentUserId = _client.CurrentUser.Id;
public AyuVoiceStateService(DiscordSocketClient client)
{
_client = client;
_currentUserId = _client.CurrentUser.Id;
var prop = _client.GetType()
.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
.First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient");
_dnetApiClient = prop.GetValue(_client, null);
_sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType().GetMethod("SendVoiceStateUpdateAsync");
var prop = _client.GetType()
.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
.First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient");
_dnetApiClient = prop.GetValue(_client, null);
_sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType().GetMethod("SendVoiceStateUpdateAsync");
_client.LeftGuild += ClientOnLeftGuild;
_client.LeftGuild += ClientOnLeftGuild;
}
private Task ClientOnLeftGuild(SocketGuild guild)
{
if (_voiceProxies.TryRemove(guild.Id, out var proxy))
{
proxy.StopGateway();
proxy.SetGateway(null);
}
private Task ClientOnLeftGuild(SocketGuild guild)
return Task.CompletedTask;
}
private Task InvokeSendVoiceStateUpdateAsync(ulong guildId, ulong? channelId = null, bool isDeafened = false, bool isMuted = false)
{
// return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted);
return (Task) _sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient, new object[] {guildId, channelId, isMuted, isDeafened, null});
}
private Task SendLeaveVoiceChannelInternalAsync(ulong guildId)
=> InvokeSendVoiceStateUpdateAsync(guildId);
private Task SendJoinVoiceChannelInternalAsync(ulong guildId, ulong channelId)
=> InvokeSendVoiceStateUpdateAsync(guildId, channelId);
private SemaphoreSlim GetVoiceGatewayLock(ulong guildId) => _voiceGatewayLocks.GetOrAdd(guildId, new SemaphoreSlim(1, 1));
private async Task LeaveVoiceChannelInternalAsync(ulong guildId)
{
var complete = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
{
if (_voiceProxies.TryRemove(guild.Id, out var proxy))
if (user is SocketGuildUser guildUser
&& guildUser.Guild.Id == guildId
&& newState.VoiceChannel?.Id is null)
{
proxy.StopGateway();
proxy.SetGateway(null);
complete.TrySetResult(true);
}
return Task.CompletedTask;
}
private Task InvokeSendVoiceStateUpdateAsync(ulong guildId, ulong? channelId = null, bool isDeafened = false, bool isMuted = false)
try
{
// return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted);
return (Task) _sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient, new object[] {guildId, channelId, isMuted, isDeafened, null});
_client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
if (_voiceProxies.TryGetValue(guildId, out var proxy))
{
_ = proxy.StopGateway();
proxy.SetGateway(null);
}
await SendLeaveVoiceChannelInternalAsync(guildId);
await Task.WhenAny(Task.Delay(1500), complete.Task);
}
finally
{
_client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
}
}
public async Task LeaveVoiceChannel(ulong guildId)
{
var gwLock = GetVoiceGatewayLock(guildId);
await gwLock.WaitAsync().ConfigureAwait(false);
try
{
await LeaveVoiceChannelInternalAsync(guildId);
}
finally
{
gwLock.Release();
}
}
private async Task<IVoiceProxy> InternalConnectToVcAsync(ulong guildId, ulong channelId)
{
var voiceStateUpdatedSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var voiceServerUpdatedSource = new TaskCompletionSource<SocketVoiceServer>(TaskCreationOptions.RunContinuationsAsynchronously);
Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
{
if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId)
{
if (newState.VoiceChannel?.Id == channelId)
voiceStateUpdatedSource.TrySetResult(newState.VoiceSessionId);
voiceStateUpdatedSource.TrySetResult(null);
}
return Task.CompletedTask;
}
private Task SendLeaveVoiceChannelInternalAsync(ulong guildId)
=> InvokeSendVoiceStateUpdateAsync(guildId);
private Task SendJoinVoiceChannelInternalAsync(ulong guildId, ulong channelId)
=> InvokeSendVoiceStateUpdateAsync(guildId, channelId);
private SemaphoreSlim GetVoiceGatewayLock(ulong guildId) => _voiceGatewayLocks.GetOrAdd(guildId, new SemaphoreSlim(1, 1));
private async Task LeaveVoiceChannelInternalAsync(ulong guildId)
Task OnVoiceServerUpdated(SocketVoiceServer data)
{
var complete = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
if (data.Guild.Id == guildId)
{
if (user is SocketGuildUser guildUser
&& guildUser.Guild.Id == guildId
&& newState.VoiceChannel?.Id is null)
{
complete.TrySetResult(true);
}
return Task.CompletedTask;
voiceServerUpdatedSource.TrySetResult(data);
}
try
{
_client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
if (_voiceProxies.TryGetValue(guildId, out var proxy))
{
_ = proxy.StopGateway();
proxy.SetGateway(null);
}
await SendLeaveVoiceChannelInternalAsync(guildId);
await Task.WhenAny(Task.Delay(1500), complete.Task);
}
finally
{
_client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
}
return Task.CompletedTask;
}
public async Task LeaveVoiceChannel(ulong guildId)
{
var gwLock = GetVoiceGatewayLock(guildId);
await gwLock.WaitAsync().ConfigureAwait(false);
try
{
await LeaveVoiceChannelInternalAsync(guildId);
}
finally
{
gwLock.Release();
}
}
private async Task<IVoiceProxy> InternalConnectToVcAsync(ulong guildId, ulong channelId)
{
var voiceStateUpdatedSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var voiceServerUpdatedSource = new TaskCompletionSource<SocketVoiceServer>(TaskCreationOptions.RunContinuationsAsynchronously);
Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
{
if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId)
{
if (newState.VoiceChannel?.Id == channelId)
voiceStateUpdatedSource.TrySetResult(newState.VoiceSessionId);
voiceStateUpdatedSource.TrySetResult(null);
}
return Task.CompletedTask;
}
Task OnVoiceServerUpdated(SocketVoiceServer data)
{
if (data.Guild.Id == guildId)
{
voiceServerUpdatedSource.TrySetResult(data);
}
return Task.CompletedTask;
}
try
try
{
_client.VoiceServerUpdated += OnVoiceServerUpdated;
_client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
await SendJoinVoiceChannelInternalAsync(guildId, channelId);
// create a delay task, how much to wait for gateway response
var delayTask = Task.Delay(2500);
// either delay or successful voiceStateUpdate
var maybeUpdateTask = Task.WhenAny(delayTask, voiceStateUpdatedSource.Task);
// either delay or successful voiceServerUpdate
var maybeServerTask = Task.WhenAny(delayTask, voiceServerUpdatedSource.Task);
// wait for both to end (max 1s) and check if either of them is a delay task
var results = await Task.WhenAll(maybeUpdateTask, maybeServerTask);
if (results[0] == delayTask || results[1] == delayTask)
{
_client.VoiceServerUpdated += OnVoiceServerUpdated;
_client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
// if either is delay, return null - connection unsuccessful
return null;
}
await SendJoinVoiceChannelInternalAsync(guildId, channelId);
// if both are succesful, that means we can safely get
// the values from completion sources
// create a delay task, how much to wait for gateway response
var delayTask = Task.Delay(2500);
var session = await voiceStateUpdatedSource.Task;
// either delay or successful voiceStateUpdate
var maybeUpdateTask = Task.WhenAny(delayTask, voiceStateUpdatedSource.Task);
// either delay or successful voiceServerUpdate
var maybeServerTask = Task.WhenAny(delayTask, voiceServerUpdatedSource.Task);
// session can be null. Means we disconnected, or connected to the wrong channel (?!)
if (session is null)
return null;
// wait for both to end (max 1s) and check if either of them is a delay task
var results = await Task.WhenAll(maybeUpdateTask, maybeServerTask);
if (results[0] == delayTask || results[1] == delayTask)
{
// if either is delay, return null - connection unsuccessful
return null;
}
// if both are succesful, that means we can safely get
// the values from completion sources
var session = await voiceStateUpdatedSource.Task;
// session can be null. Means we disconnected, or connected to the wrong channel (?!)
if (session is null)
return null;
var voiceServerData = await voiceServerUpdatedSource.Task;
var voiceServerData = await voiceServerUpdatedSource.Task;
VoiceGateway CreateVoiceGatewayLocal() =>
new VoiceGateway(
guildId,
_currentUserId,
session,
voiceServerData.Token,
voiceServerData.Endpoint
);
var current = _voiceProxies.AddOrUpdate(
VoiceGateway CreateVoiceGatewayLocal() =>
new VoiceGateway(
guildId,
(gid) => new VoiceProxy(CreateVoiceGatewayLocal()),
(gid, currentProxy) =>
{
_ = currentProxy.StopGateway();
currentProxy.SetGateway(CreateVoiceGatewayLocal());
return currentProxy;
}
_currentUserId,
session,
voiceServerData.Token,
voiceServerData.Endpoint
);
_ = current.StartGateway(); // don't await, this blocks until gateway is closed
return current;
}
finally
{
_client.VoiceServerUpdated -= OnVoiceServerUpdated;
_client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
}
}
var current = _voiceProxies.AddOrUpdate(
guildId,
(gid) => new VoiceProxy(CreateVoiceGatewayLocal()),
(gid, currentProxy) =>
{
_ = currentProxy.StopGateway();
currentProxy.SetGateway(CreateVoiceGatewayLocal());
return currentProxy;
}
);
public async Task<IVoiceProxy> JoinVoiceChannel(ulong guildId, ulong channelId, bool forceReconnect = true)
_ = current.StartGateway(); // don't await, this blocks until gateway is closed
return current;
}
finally
{
var gwLock = GetVoiceGatewayLock(guildId);
await gwLock.WaitAsync().ConfigureAwait(false);
try
{
await LeaveVoiceChannelInternalAsync(guildId);
return await InternalConnectToVcAsync(guildId, channelId);
}
finally
{
gwLock.Release();
}
_client.VoiceServerUpdated -= OnVoiceServerUpdated;
_client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
}
public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy)
=> _voiceProxies.TryGetValue(guildId, out proxy);
}
public async Task<IVoiceProxy> JoinVoiceChannel(ulong guildId, ulong channelId, bool forceReconnect = true)
{
var gwLock = GetVoiceGatewayLock(guildId);
await gwLock.WaitAsync().ConfigureAwait(false);
try
{
await LeaveVoiceChannelInternalAsync(guildId);
return await InternalConnectToVcAsync(guildId, channelId);
}
finally
{
gwLock.Release();
}
}
public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy)
=> _voiceProxies.TryGetValue(guildId, out proxy);
}

View File

@@ -1,42 +1,39 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Discord;
using NadekoBot.Common;
using NadekoBot.Modules.Music;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Music.Services
{
public interface IMusicService : IPlaceholderProvider
{
/// <summary>
/// Leave voice channel in the specified guild if it's connected to one
/// </summary>
/// <param name="guildId">Id of the guild</param>
public Task LeaveVoiceChannelAsync(ulong guildId);
namespace NadekoBot.Modules.Music.Services;
/// <summary>
/// Joins the voice channel with the specified id
/// </summary>
/// <param name="guildId">Id of the guild where the voice channel is</param>
/// <param name="voiceChannelId">Id of the voice channel</param>
public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId);
public interface IMusicService : IPlaceholderProvider
{
/// <summary>
/// Leave voice channel in the specified guild if it's connected to one
/// </summary>
/// <param name="guildId">Id of the guild</param>
public Task LeaveVoiceChannelAsync(ulong guildId);
/// <summary>
/// Joins the voice channel with the specified id
/// </summary>
/// <param name="guildId">Id of the guild where the voice channel is</param>
/// <param name="voiceChannelId">Id of the voice channel</param>
public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId);
Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel);
bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer);
Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string playlistId, string queuer);
Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer);
Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer);
Task<IUserMessage?> SendToOutputAsync(ulong guildId, IEmbedBuilder embed);
Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId);
Task<IList<(string Title, string Url)>> SearchVideosAsync(string query);
Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId);
Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType);
Task SetVolumeAsync(ulong guildId, int value);
Task<bool> ToggleAutoDisconnectAsync(ulong guildId);
Task<QualityPreset> GetMusicQualityAsync(ulong guildId);
Task SetMusicQualityAsync(ulong guildId, QualityPreset preset);
}
}
Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel);
bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer);
Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string playlistId, string queuer);
Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer);
Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer);
Task<IUserMessage?> SendToOutputAsync(ulong guildId, IEmbedBuilder embed);
Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId);
Task<IList<(string Title, string Url)>> SearchVideosAsync(string query);
Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId);
Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType);
Task SetVolumeAsync(ulong guildId, int value);
Task<bool> ToggleAutoDisconnectAsync(ulong guildId);
Task<QualityPreset> GetMusicQualityAsync(ulong guildId);
Task SetMusicQualityAsync(ulong guildId, QualityPreset preset);
}

View File

@@ -1,462 +1,455 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Db;
using NadekoBot.Modules.Music;
using NadekoBot.Db.Models;
using NadekoBot.Extensions;
using Serilog;
namespace NadekoBot.Modules.Music.Services
namespace NadekoBot.Modules.Music.Services;
public sealed class MusicService : IMusicService
{
public sealed class MusicService : IMusicService
private readonly AyuVoiceStateService _voiceStateService;
private readonly ITrackResolveProvider _trackResolveProvider;
private readonly DbService _db;
private readonly IYoutubeResolver _ytResolver;
private readonly ILocalTrackResolver _localResolver;
private readonly ISoundcloudResolver _scResolver;
private readonly DiscordSocketClient _client;
private readonly IBotStrings _strings;
private readonly IGoogleApiService _googleApiService;
private readonly YtLoader _ytLoader;
private readonly IEmbedBuilderService _eb;
private readonly ConcurrentDictionary<ulong, IMusicPlayer> _players;
private readonly ConcurrentDictionary<ulong, (ITextChannel Default, ITextChannel? Override)> _outputChannels;
private readonly ConcurrentDictionary<ulong, MusicPlayerSettings> _settings;
public MusicService(AyuVoiceStateService voiceStateService, ITrackResolveProvider trackResolveProvider,
DbService db, IYoutubeResolver ytResolver, ILocalTrackResolver localResolver, ISoundcloudResolver scResolver,
DiscordSocketClient client, IBotStrings strings, IGoogleApiService googleApiService, YtLoader ytLoader,
IEmbedBuilderService eb)
{
private readonly AyuVoiceStateService _voiceStateService;
private readonly ITrackResolveProvider _trackResolveProvider;
private readonly DbService _db;
private readonly IYoutubeResolver _ytResolver;
private readonly ILocalTrackResolver _localResolver;
private readonly ISoundcloudResolver _scResolver;
private readonly DiscordSocketClient _client;
private readonly IBotStrings _strings;
private readonly IGoogleApiService _googleApiService;
private readonly YtLoader _ytLoader;
private readonly IEmbedBuilderService _eb;
_voiceStateService = voiceStateService;
_trackResolveProvider = trackResolveProvider;
_db = db;
_ytResolver = ytResolver;
_localResolver = localResolver;
_scResolver = scResolver;
_client = client;
_strings = strings;
_googleApiService = googleApiService;
_ytLoader = ytLoader;
_eb = eb;
private readonly ConcurrentDictionary<ulong, IMusicPlayer> _players;
private readonly ConcurrentDictionary<ulong, (ITextChannel Default, ITextChannel? Override)> _outputChannels;
private readonly ConcurrentDictionary<ulong, MusicPlayerSettings> _settings;
public MusicService(AyuVoiceStateService voiceStateService, ITrackResolveProvider trackResolveProvider,
DbService db, IYoutubeResolver ytResolver, ILocalTrackResolver localResolver, ISoundcloudResolver scResolver,
DiscordSocketClient client, IBotStrings strings, IGoogleApiService googleApiService, YtLoader ytLoader,
IEmbedBuilderService eb)
{
_voiceStateService = voiceStateService;
_trackResolveProvider = trackResolveProvider;
_db = db;
_ytResolver = ytResolver;
_localResolver = localResolver;
_scResolver = scResolver;
_client = client;
_strings = strings;
_googleApiService = googleApiService;
_ytLoader = ytLoader;
_eb = eb;
_players = new ConcurrentDictionary<ulong, IMusicPlayer>();
_outputChannels = new ConcurrentDictionary<ulong, (ITextChannel, ITextChannel?)>();
_settings = new ConcurrentDictionary<ulong, MusicPlayerSettings>();
_players = new ConcurrentDictionary<ulong, IMusicPlayer>();
_outputChannels = new ConcurrentDictionary<ulong, (ITextChannel, ITextChannel?)>();
_settings = new ConcurrentDictionary<ulong, MusicPlayerSettings>();
_client.LeftGuild += ClientOnLeftGuild;
}
_client.LeftGuild += ClientOnLeftGuild;
}
private void DisposeMusicPlayer(IMusicPlayer musicPlayer)
private void DisposeMusicPlayer(IMusicPlayer musicPlayer)
{
musicPlayer.Kill();
_ = Task.Delay(10_000).ContinueWith(_ => musicPlayer.Dispose());
}
private void RemoveMusicPlayer(ulong guildId)
{
_outputChannels.TryRemove(guildId, out _);
if (_players.TryRemove(guildId, out var mp))
{
musicPlayer.Kill();
_ = Task.Delay(10_000).ContinueWith(_ => musicPlayer.Dispose());
DisposeMusicPlayer(mp);
}
}
private void RemoveMusicPlayer(ulong guildId)
{
_outputChannels.TryRemove(guildId, out _);
if (_players.TryRemove(guildId, out var mp))
{
DisposeMusicPlayer(mp);
}
}
private Task ClientOnLeftGuild(SocketGuild guild)
{
RemoveMusicPlayer(guild.Id);
return Task.CompletedTask;
}
private Task ClientOnLeftGuild(SocketGuild guild)
{
RemoveMusicPlayer(guild.Id);
return Task.CompletedTask;
}
public async Task LeaveVoiceChannelAsync(ulong guildId)
{
RemoveMusicPlayer(guildId);
await _voiceStateService.LeaveVoiceChannel(guildId);
}
public async Task LeaveVoiceChannelAsync(ulong guildId)
{
RemoveMusicPlayer(guildId);
await _voiceStateService.LeaveVoiceChannel(guildId);
}
public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId)
=> _voiceStateService.JoinVoiceChannel(guildId, voiceChannelId);
public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId)
=> _voiceStateService.JoinVoiceChannel(guildId, voiceChannelId);
public async Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel)
{
var newPLayer = await CreateMusicPlayerInternalAsync(contextChannel.GuildId, contextChannel);
if (newPLayer is null)
return null;
public async Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel)
{
var newPLayer = await CreateMusicPlayerInternalAsync(contextChannel.GuildId, contextChannel);
if (newPLayer is null)
return null;
return _players.GetOrAdd(contextChannel.GuildId, newPLayer);
return _players.GetOrAdd(contextChannel.GuildId, newPLayer);
}
public bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer)
=> _players.TryGetValue(guildId, out musicPlayer);
public async Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer)
{
var count = 0;
await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query))
{
if (mp.IsKilled)
break;
mp.EnqueueTrack(track, queuer);
++count;
}
public bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer)
=> _players.TryGetValue(guildId, out musicPlayer);
return count;
}
public async Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer)
public async Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer)
{
await foreach (var track in _localResolver.ResolveDirectoryAsync(dirPath))
{
var count = 0;
await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query))
{
if (mp.IsKilled)
break;
mp.EnqueueTrack(track, queuer);
++count;
}
return count;
}
public async Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer)
{
await foreach (var track in _localResolver.ResolveDirectoryAsync(dirPath))
{
if (mp.IsKilled)
break;
if (mp.IsKilled)
break;
mp.EnqueueTrack(track, queuer);
}
mp.EnqueueTrack(track, queuer);
}
}
public async Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer)
public async Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer)
{
var i = 0;
await foreach (var track in _scResolver.ResolvePlaylistAsync(playlist))
{
var i = 0;
await foreach (var track in _scResolver.ResolvePlaylistAsync(playlist))
{
if (mp.IsKilled)
break;
if (mp.IsKilled)
break;
mp.EnqueueTrack(track, queuer);
++i;
}
return i;
mp.EnqueueTrack(track, queuer);
++i;
}
private async Task<IMusicPlayer?> CreateMusicPlayerInternalAsync(ulong guildId, ITextChannel defaultChannel)
return i;
}
private async Task<IMusicPlayer?> CreateMusicPlayerInternalAsync(ulong guildId, ITextChannel defaultChannel)
{
var queue = new MusicQueue();
var resolver = _trackResolveProvider;
if (!_voiceStateService.TryGetProxy(guildId, out var proxy))
{
var queue = new MusicQueue();
var resolver = _trackResolveProvider;
return null;
}
if (!_voiceStateService.TryGetProxy(guildId, out var proxy))
var settings = await GetSettingsInternalAsync(guildId);
ITextChannel? overrideChannel = null;
if (settings.MusicChannelId is ulong channelId)
{
overrideChannel = _client.GetGuild(guildId)?.GetTextChannel(channelId);
if (overrideChannel is null)
{
return null;
}
var settings = await GetSettingsInternalAsync(guildId);
ITextChannel? overrideChannel = null;
if (settings.MusicChannelId is ulong channelId)
{
overrideChannel = _client.GetGuild(guildId)?.GetTextChannel(channelId);
if (overrideChannel is null)
{
Log.Warning("Saved music output channel doesn't exist, falling back to current channel");
}
Log.Warning("Saved music output channel doesn't exist, falling back to current channel");
}
}
_outputChannels[guildId] = (defaultChannel, overrideChannel);
_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);
mp.SetRepeat(settings.PlayerRepeat);
if (settings.Volume >= 0 && settings.Volume <= 100)
{
mp.SetVolume(settings.Volume);
}
else
{
Log.Error("Saved Volume is outside of valid range >= 0 && <=100 ({Volume})", settings.Volume);
}
mp.OnCompleted += OnTrackCompleted(guildId);
mp.OnStarted += OnTrackStarted(guildId);
mp.OnQueueStopped += OnQueueStopped(guildId);
return mp;
}
public Task<IUserMessage?> SendToOutputAsync(ulong guildId, IEmbedBuilder embed)
if (settings.Volume >= 0 && settings.Volume <= 100)
{
if (_outputChannels.TryGetValue(guildId, out var chan))
return (chan.Default ?? chan.Override).EmbedAsync(embed);
return Task.FromResult<IUserMessage?>(null);
mp.SetVolume(settings.Volume);
}
private Func<IMusicPlayer, IQueuedTrackInfo, Task> OnTrackCompleted(ulong guildId)
else
{
IUserMessage? lastFinishedMessage = null;
return async (mp, trackInfo) =>
{
_ = lastFinishedMessage?.DeleteAsync();
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(GetText(guildId, strs.finished_song), Music.MusicIconUrl)
.WithDescription(trackInfo.PrettyName())
.WithFooter(trackInfo.PrettyTotalTime());
lastFinishedMessage = await SendToOutputAsync(guildId, embed);
};
Log.Error("Saved Volume is outside of valid range >= 0 && <=100 ({Volume})", settings.Volume);
}
private Func<IMusicPlayer, IQueuedTrackInfo, int, Task> OnTrackStarted(ulong guildId)
mp.OnCompleted += OnTrackCompleted(guildId);
mp.OnStarted += OnTrackStarted(guildId);
mp.OnQueueStopped += OnQueueStopped(guildId);
return mp;
}
public Task<IUserMessage?> SendToOutputAsync(ulong guildId, IEmbedBuilder embed)
{
if (_outputChannels.TryGetValue(guildId, out var chan))
return (chan.Default ?? chan.Override).EmbedAsync(embed);
return Task.FromResult<IUserMessage?>(null);
}
private Func<IMusicPlayer, IQueuedTrackInfo, Task> OnTrackCompleted(ulong guildId)
{
IUserMessage? lastFinishedMessage = null;
return async (mp, trackInfo) =>
{
IUserMessage? lastPlayingMessage = null;
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()}");
_ = lastFinishedMessage?.DeleteAsync();
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(GetText(guildId, strs.finished_song), Music.MusicIconUrl)
.WithDescription(trackInfo.PrettyName())
.WithFooter(trackInfo.PrettyTotalTime());
lastPlayingMessage = await SendToOutputAsync(guildId, embed);
};
}
lastFinishedMessage = await SendToOutputAsync(guildId, embed);
};
}
private Func<IMusicPlayer, Task> OnQueueStopped(ulong guildId)
=> (mp) =>
{
if (_settings.TryGetValue(guildId, out var settings))
{
if (settings.AutoDisconnect)
{
return LeaveVoiceChannelAsync(guildId);
}
}
return Task.CompletedTask;
};
// this has to be done because dragging bot to another vc isn't supported yet
public async Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId)
private Func<IMusicPlayer, IQueuedTrackInfo, int, Task> OnTrackStarted(ulong guildId)
{
IUserMessage? lastPlayingMessage = null;
return async (mp, trackInfo, index) =>
{
if (!TryGetMusicPlayer(guildId, out var mp))
{
return false;
}
_ = 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()}");
if (mp.IsStopped)
{
if (!_voiceStateService.TryGetProxy(guildId, out var proxy)
|| proxy.State == VoiceProxy.VoiceProxyState.Stopped)
{
await JoinVoiceChannelAsync(guildId, voiceChannelId);
}
}
lastPlayingMessage = await SendToOutputAsync(guildId, embed);
};
}
mp.Next();
return true;
}
private async Task<IList<(string Title, string Url)>> SearchYtLoaderVideosAsync(string query)
{
var result = await _ytLoader.LoadResultsAsync(query);
return result.Select(x => (x.Title, x.Url)).ToList();
}
private async Task<IList<(string Title, string Url)>> SearchGoogleApiVideosAsync(string query)
{
var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5);
return result.Select(x => (x.Name, x.Url)).ToList();
}
public async Task<IList<(string Title, string Url)>> SearchVideosAsync(string query)
{
try
{
IList<(string, string)> videos = await SearchYtLoaderVideosAsync(query);
if (videos.Count > 0)
{
return videos;
}
}
catch (Exception ex)
{
Log.Warning("Failed geting videos with YtLoader: {ErrorMessage}", ex.Message);
}
try
{
return await SearchGoogleApiVideosAsync(query);
}
catch (Exception ex)
{
Log.Warning("Failed getting video results with Google Api. " +
"Probably google api key missing: {ErrorMessage}", ex.Message);
}
return Array.Empty<(string, string)>();
}
private string GetText(ulong guildId, LocStr str)
=> _strings.GetText(str, guildId);
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders()
{
// random song that's playing
yield return ("%music.playing%", () =>
{
var randomPlayingTrack = _players
.Select(x => x.Value.GetCurrentTrack(out _))
.Where(x => x is not null)
.Shuffle()
.FirstOrDefault();
if (randomPlayingTrack is null)
return "-";
return randomPlayingTrack.Title;
});
// number of servers currently listening to music
yield return ("%music.servers%", () =>
{
var count = _players
.Select(x => x.Value.GetCurrentTrack(out _))
.Count(x => x is not null);
return count.ToString();
});
yield return ("%music.queued%", () =>
{
var count = _players
.Sum(x => x.Value.GetQueuedTracks().Count);
return count.ToString();
});
}
#region Settings
private async Task<MusicPlayerSettings> GetSettingsInternalAsync(ulong guildId)
private Func<IMusicPlayer, Task> OnQueueStopped(ulong guildId)
=> (mp) =>
{
if (_settings.TryGetValue(guildId, out var settings))
return settings;
using var uow = _db.GetDbContext();
var toReturn = _settings[guildId] = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
await uow.SaveChangesAsync();
return toReturn;
}
private async Task ModifySettingsInternalAsync<TState>(
ulong guildId,
Action<MusicPlayerSettings, TState> action,
TState state)
{
using var uow = _db.GetDbContext();
var ms = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
action(ms, state);
await uow.SaveChangesAsync();
_settings[guildId] = ms;
}
public async Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId)
{
if (channelId is null)
{
await UnsetMusicChannelAsync(guildId);
return true;
if (settings.AutoDisconnect)
{
return LeaveVoiceChannelAsync(guildId);
}
}
var channel = _client.GetGuild(guildId)?.GetTextChannel(channelId.Value);
if (channel is null)
return false;
await ModifySettingsInternalAsync(guildId, (settings, chId) =>
return Task.CompletedTask;
};
// this has to be done because dragging bot to another vc isn't supported yet
public async Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId)
{
if (!TryGetMusicPlayer(guildId, out var mp))
{
return false;
}
if (mp.IsStopped)
{
if (!_voiceStateService.TryGetProxy(guildId, out var proxy)
|| proxy.State == VoiceProxy.VoiceProxyState.Stopped)
{
settings.MusicChannelId = chId;
}, channelId);
await JoinVoiceChannelAsync(guildId, voiceChannelId);
}
}
_outputChannels.AddOrUpdate(guildId,
(channel, channel),
(key, old) => (old.Default, channel));
mp.Next();
return true;
}
private async Task<IList<(string Title, string Url)>> SearchYtLoaderVideosAsync(string query)
{
var result = await _ytLoader.LoadResultsAsync(query);
return result.Select(x => (x.Title, x.Url)).ToList();
}
private async Task<IList<(string Title, string Url)>> SearchGoogleApiVideosAsync(string query)
{
var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5);
return result.Select(x => (x.Name, x.Url)).ToList();
}
public async Task<IList<(string Title, string Url)>> SearchVideosAsync(string query)
{
try
{
IList<(string, string)> videos = await SearchYtLoaderVideosAsync(query);
if (videos.Count > 0)
{
return videos;
}
}
catch (Exception ex)
{
Log.Warning("Failed geting videos with YtLoader: {ErrorMessage}", ex.Message);
}
try
{
return await SearchGoogleApiVideosAsync(query);
}
catch (Exception ex)
{
Log.Warning("Failed getting video results with Google Api. " +
"Probably google api key missing: {ErrorMessage}", ex.Message);
}
return Array.Empty<(string, string)>();
}
private string GetText(ulong guildId, LocStr str)
=> _strings.GetText(str, guildId);
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders()
{
// random song that's playing
yield return ("%music.playing%", () =>
{
var randomPlayingTrack = _players
.Select(x => x.Value.GetCurrentTrack(out _))
.Where(x => x is not null)
.Shuffle()
.FirstOrDefault();
if (randomPlayingTrack is null)
return "-";
return randomPlayingTrack.Title;
});
// number of servers currently listening to music
yield return ("%music.servers%", () =>
{
var count = _players
.Select(x => x.Value.GetCurrentTrack(out _))
.Count(x => x is not null);
return count.ToString();
});
yield return ("%music.queued%", () =>
{
var count = _players
.Sum(x => x.Value.GetQueuedTracks().Count);
return count.ToString();
});
}
#region Settings
private async Task<MusicPlayerSettings> GetSettingsInternalAsync(ulong guildId)
{
if (_settings.TryGetValue(guildId, out var settings))
return settings;
using var uow = _db.GetDbContext();
var toReturn = _settings[guildId] = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
await uow.SaveChangesAsync();
return toReturn;
}
private async Task ModifySettingsInternalAsync<TState>(
ulong guildId,
Action<MusicPlayerSettings, TState> action,
TState state)
{
using var uow = _db.GetDbContext();
var ms = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
action(ms, state);
await uow.SaveChangesAsync();
_settings[guildId] = ms;
}
public async Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId)
{
if (channelId is null)
{
await UnsetMusicChannelAsync(guildId);
return true;
}
public async Task UnsetMusicChannelAsync(ulong guildId)
{
await ModifySettingsInternalAsync(guildId, (settings, _) =>
{
settings.MusicChannelId = null;
}, (ulong?)null);
if (_outputChannels.TryGetValue(guildId, out var old))
_outputChannels[guildId] = (old.Default, null);
}
public async Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType)
{
await ModifySettingsInternalAsync(guildId, (settings, type) =>
{
settings.PlayerRepeat = type;
}, repeatType);
if (TryGetMusicPlayer(guildId, out var mp))
mp.SetRepeat(repeatType);
}
public async Task SetVolumeAsync(ulong guildId, int value)
{
if (value < 0 || value > 100)
throw new ArgumentOutOfRangeException(nameof(value));
await ModifySettingsInternalAsync(guildId, (settings, newValue) =>
{
settings.Volume = newValue;
}, value);
var channel = _client.GetGuild(guildId)?.GetTextChannel(channelId.Value);
if (channel is null)
return false;
await ModifySettingsInternalAsync(guildId, (settings, chId) =>
{
settings.MusicChannelId = chId;
}, channelId);
_outputChannels.AddOrUpdate(guildId,
(channel, channel),
(key, old) => (old.Default, channel));
if (TryGetMusicPlayer(guildId, out var mp))
mp.SetVolume(value);
}
public async Task<bool> ToggleAutoDisconnectAsync(ulong guildId)
{
var newState = false;
await ModifySettingsInternalAsync(guildId, (settings, _) =>
{
newState = settings.AutoDisconnect = !settings.AutoDisconnect;
}, default(object));
return newState;
}
public async Task<QualityPreset> GetMusicQualityAsync(ulong guildId)
{
using var uow = _db.GetDbContext();
var settings = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
return settings.QualityPreset;
}
public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset)
{
return ModifySettingsInternalAsync(guildId, (settings, _) =>
{
settings.QualityPreset = preset;
}, preset);
}
#endregion
return true;
}
public async Task UnsetMusicChannelAsync(ulong guildId)
{
await ModifySettingsInternalAsync(guildId, (settings, _) =>
{
settings.MusicChannelId = null;
}, (ulong?)null);
if (_outputChannels.TryGetValue(guildId, out var old))
_outputChannels[guildId] = (old.Default, null);
}
public async Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType)
{
await ModifySettingsInternalAsync(guildId, (settings, type) =>
{
settings.PlayerRepeat = type;
}, repeatType);
if (TryGetMusicPlayer(guildId, out var mp))
mp.SetRepeat(repeatType);
}
public async Task SetVolumeAsync(ulong guildId, int value)
{
if (value < 0 || value > 100)
throw new ArgumentOutOfRangeException(nameof(value));
await ModifySettingsInternalAsync(guildId, (settings, newValue) =>
{
settings.Volume = newValue;
}, value);
if (TryGetMusicPlayer(guildId, out var mp))
mp.SetVolume(value);
}
public async Task<bool> ToggleAutoDisconnectAsync(ulong guildId)
{
var newState = false;
await ModifySettingsInternalAsync(guildId, (settings, _) =>
{
newState = settings.AutoDisconnect = !settings.AutoDisconnect;
}, default(object));
return newState;
}
public async Task<QualityPreset> GetMusicQualityAsync(ulong guildId)
{
using var uow = _db.GetDbContext();
var settings = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
return settings.QualityPreset;
}
public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset)
{
return ModifySettingsInternalAsync(guildId, (settings, _) =>
{
settings.QualityPreset = preset;
}, preset);
}
#endregion
}

View File

@@ -1,75 +1,71 @@
using System;
namespace NadekoBot.Modules.Music.Services;
namespace NadekoBot.Modules.Music.Services
public sealed partial class YtLoader
{
public sealed partial class YtLoader
public class InitRange
{
public string Start { get; set; }
public string End { get; set; }
}
public class InitRange
public class IndexRange
{
public string Start { get; set; }
public string End { get; set; }
}
public class ColorInfo
{
public string Primaries { get; set; }
public string TransferCharacteristics { get; set; }
public string MatrixCoefficients { get; set; }
}
public class YtAdaptiveFormat
{
public int Itag { get; set; }
public string MimeType { get; set; }
public int Bitrate { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public InitRange InitRange { get; set; }
public IndexRange IndexRange { get; set; }
public string LastModified { get; set; }
public string ContentLength { get; set; }
public string Quality { get; set; }
public int Fps { get; set; }
public string QualityLabel { get; set; }
public string ProjectionType { get; set; }
public int AverageBitrate { get; set; }
public ColorInfo ColorInfo { get; set; }
public string ApproxDurationMs { get; set; }
public string SignatureCipher { get; set; }
}
public abstract class TrackInfo
{
public abstract string Url { get; }
public abstract string Title { get; }
public abstract TimeSpan Duration { get; }
}
public sealed class YtTrackInfo : TrackInfo
{
private const string BaseYoutubeUrl = "https://youtube.com/watch?v=";
public override string Url { get; }
public override string Title { get; }
public override TimeSpan Duration { get; }
private readonly string _videoId;
public YtTrackInfo(string title, string videoId, TimeSpan duration)
{
public string Start { get; set; }
public string End { get; set; }
}
Title = title;
Url = BaseYoutubeUrl + videoId;
Duration = duration;
public class IndexRange
{
public string Start { get; set; }
public string End { get; set; }
}
public class ColorInfo
{
public string Primaries { get; set; }
public string TransferCharacteristics { get; set; }
public string MatrixCoefficients { get; set; }
}
public class YtAdaptiveFormat
{
public int Itag { get; set; }
public string MimeType { get; set; }
public int Bitrate { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public InitRange InitRange { get; set; }
public IndexRange IndexRange { get; set; }
public string LastModified { get; set; }
public string ContentLength { get; set; }
public string Quality { get; set; }
public int Fps { get; set; }
public string QualityLabel { get; set; }
public string ProjectionType { get; set; }
public int AverageBitrate { get; set; }
public ColorInfo ColorInfo { get; set; }
public string ApproxDurationMs { get; set; }
public string SignatureCipher { get; set; }
}
public abstract class TrackInfo
{
public abstract string Url { get; }
public abstract string Title { get; }
public abstract TimeSpan Duration { get; }
}
public sealed class YtTrackInfo : TrackInfo
{
private const string BaseYoutubeUrl = "https://youtube.com/watch?v=";
public override string Url { get; }
public override string Title { get; }
public override TimeSpan Duration { get; }
private readonly string _videoId;
public YtTrackInfo(string title, string videoId, TimeSpan duration)
{
Title = title;
Url = BaseYoutubeUrl + videoId;
Duration = duration;
_videoId = videoId;
}
_videoId = videoId;
}
}
}

View File

@@ -1,140 +1,133 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Globalization;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Discord.Net;
using Serilog;
namespace NadekoBot.Modules.Music.Services
namespace NadekoBot.Modules.Music.Services;
public sealed partial class YtLoader
{
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 readonly IHttpClientFactory _httpFactory;
private static readonly byte[] YT_RESULT_INITIAL_DATA = Encoding.UTF8.GetBytes("var ytInitialData = ");
private static readonly byte[] YT_RESULT_JSON_END = Encoding.UTF8.GetBytes(";<");
private static readonly string[] durationFormats = new[]
{
@"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss", @"hhh\:mm\:ss"
};
private static readonly string[] durationFormats = new[]
{
@"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss", @"hhh\:mm\:ss"
};
public YtLoader(IHttpClientFactory httpFactory)
public YtLoader(IHttpClientFactory httpFactory)
{
_httpFactory = httpFactory;
}
// public async Task<TrackInfo> LoadTrackByIdAsync(string videoId)
// {
// using var http = new HttpClient();
// http.DefaultRequestHeaders.Add("X-YouTube-Client-Name", "1");
// http.DefaultRequestHeaders.Add("X-YouTube-Client-Version", "2.20210520.09.00");
// http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
//
// var responseString = await http.GetStringAsync($"https://youtube.com?" +
// $"pbj=1" +
// $"&hl=en" +
// $"&v=" + videoId);
//
// var jsonDoc = JsonDocument.Parse(responseString).RootElement;
// var elem = jsonDoc.EnumerateArray()
// .FirstOrDefault(x => x.TryGetProperty("page", out var elem) && elem.GetString() == "watch");
//
// var formatsJsonArray = elem.GetProperty("streamingdata")
// .GetProperty("formats")
// .GetRawText();
//
// var formats = JsonSerializer.Deserialize<List<YtAdaptiveFormat>>(formatsJsonArray);
// var result = formats
// .Where(x => x.MimeType.StartsWith("audio/"))
// .OrderByDescending(x => x.Bitrate)
// .FirstOrDefault();
//
// if (result is null)
// return null;
//
// return new YtTrackInfo("1", "2", TimeSpan.Zero);
// }
public async Task<IList<TrackInfo>> LoadResultsAsync(string query)
{
query = Uri.EscapeDataString(query);
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
byte[] response;
try
{
_httpFactory = httpFactory;
response = await http.GetByteArrayAsync($"https://youtube.com/results?hl=en&search_query={query}");
}
catch (HttpRequestException ex)
{
Log.Warning("Unable to retrieve data with YtLoader: {ErrorMessage}", ex.Message);
return null;
}
// public async Task<TrackInfo> LoadTrackByIdAsync(string videoId)
// {
// using var http = new HttpClient();
// http.DefaultRequestHeaders.Add("X-YouTube-Client-Name", "1");
// http.DefaultRequestHeaders.Add("X-YouTube-Client-Version", "2.20210520.09.00");
// http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
//
// var responseString = await http.GetStringAsync($"https://youtube.com?" +
// $"pbj=1" +
// $"&hl=en" +
// $"&v=" + videoId);
//
// var jsonDoc = JsonDocument.Parse(responseString).RootElement;
// var elem = jsonDoc.EnumerateArray()
// .FirstOrDefault(x => x.TryGetProperty("page", out var elem) && elem.GetString() == "watch");
//
// var formatsJsonArray = elem.GetProperty("streamingdata")
// .GetProperty("formats")
// .GetRawText();
//
// var formats = JsonSerializer.Deserialize<List<YtAdaptiveFormat>>(formatsJsonArray);
// var result = formats
// .Where(x => x.MimeType.StartsWith("audio/"))
// .OrderByDescending(x => x.Bitrate)
// .FirstOrDefault();
//
// if (result is null)
// return null;
//
// return new YtTrackInfo("1", "2", TimeSpan.Zero);
// }
// there is a lot of useless html above the script tag, however if html gets significantly reduced
// this will result in the json being cut off
public async Task<IList<TrackInfo>> LoadResultsAsync(string query)
var mem = GetScriptResponseSpan(response);
var root = JsonDocument.Parse(mem).RootElement;
var tracksJsonItems = root
.GetProperty("contents")
.GetProperty("twoColumnSearchResultsRenderer")
.GetProperty("primaryContents")
.GetProperty("sectionListRenderer")
.GetProperty("contents")
[0]
.GetProperty("itemSectionRenderer")
.GetProperty("contents")
.EnumerateArray();
var tracks = new List<TrackInfo>();
foreach (var track in tracksJsonItems)
{
query = Uri.EscapeDataString(query);
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
if(!track.TryGetProperty("videoRenderer", out var elem))
continue;
byte[] response;
try
{
response = await http.GetByteArrayAsync($"https://youtube.com/results?hl=en&search_query={query}");
}
catch (HttpRequestException ex)
{
Log.Warning("Unable to retrieve data with YtLoader: {ErrorMessage}", ex.Message);
return null;
}
var videoId = elem.GetProperty("videoId").GetString();
// var thumb = elem.GetProperty("thumbnail").GetProperty("thumbnails")[0].GetProperty("url").GetString();
var title = elem.GetProperty("title").GetProperty("runs")[0].GetProperty("text").GetString();
var durationString = elem.GetProperty("lengthText").GetProperty("simpleText").GetString();
// there is a lot of useless html above the script tag, however if html gets significantly reduced
// this will result in the json being cut off
var mem = GetScriptResponseSpan(response);
var root = JsonDocument.Parse(mem).RootElement;
var tracksJsonItems = root
.GetProperty("contents")
.GetProperty("twoColumnSearchResultsRenderer")
.GetProperty("primaryContents")
.GetProperty("sectionListRenderer")
.GetProperty("contents")
[0]
.GetProperty("itemSectionRenderer")
.GetProperty("contents")
.EnumerateArray();
var tracks = new List<TrackInfo>();
foreach (var track in tracksJsonItems)
{
if(!track.TryGetProperty("videoRenderer", out var elem))
continue;
var videoId = elem.GetProperty("videoId").GetString();
// var thumb = elem.GetProperty("thumbnail").GetProperty("thumbnails")[0].GetProperty("url").GetString();
var title = elem.GetProperty("title").GetProperty("runs")[0].GetProperty("text").GetString();
var durationString = elem.GetProperty("lengthText").GetProperty("simpleText").GetString();
if (!TimeSpan.TryParseExact(durationString, durationFormats, CultureInfo.InvariantCulture,
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;
{
Log.Warning("Cannot parse duration: {DurationString}", durationString);
continue;
}
tracks.Add(new YtTrackInfo(title, videoId, duration));
if (tracks.Count >= 5)
break;
}
return tracks;
}
return tracks;
}
private Memory<byte> GetScriptResponseSpan(byte[] response)
{
var responseSpan = response.AsSpan().Slice(140_000);
var startIndex = responseSpan.IndexOf(YT_RESULT_INITIAL_DATA);
if (startIndex == -1)
return null; // todo future try selecting html
startIndex += YT_RESULT_INITIAL_DATA.Length;
private Memory<byte> GetScriptResponseSpan(byte[] response)
{
var responseSpan = response.AsSpan().Slice(140_000);
var startIndex = responseSpan.IndexOf(YT_RESULT_INITIAL_DATA);
if (startIndex == -1)
return null; // todo future try selecting html
startIndex += YT_RESULT_INITIAL_DATA.Length;
var endIndex = 140_000 + startIndex + responseSpan.Slice(startIndex + 20_000).IndexOf(YT_RESULT_JSON_END) + 20_000;
startIndex += 140_000;
return response.AsMemory(
startIndex,
endIndex - startIndex
);
}
var endIndex = 140_000 + startIndex + responseSpan.Slice(startIndex + 20_000).IndexOf(YT_RESULT_JSON_END) + 20_000;
startIndex += 140_000;
return response.AsMemory(
startIndex,
endIndex - startIndex
);
}
}