mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04:00
Killed history
This commit is contained in:
221
NadekoBot.Core/Modules/Music/Services/AyuVoiceStateService.cs
Normal file
221
NadekoBot.Core/Modules/Music/Services/AyuVoiceStateService.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ayu.Discord.Voice;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Core.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Services
|
||||
{
|
||||
public sealed class AyuVoiceStateService : INService
|
||||
{
|
||||
// public delegate Task VoiceProxyUpdatedDelegate(ulong guildId, IVoiceProxy proxy);
|
||||
// public event VoiceProxyUpdatedDelegate OnVoiceProxyUpdate = delegate { return Task.CompletedTask; };
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, IVoiceProxy> _voiceProxies = new ConcurrentDictionary<ulong, IVoiceProxy>();
|
||||
private readonly ConcurrentDictionary<ulong, SemaphoreSlim> _voiceGatewayLocks = new ConcurrentDictionary<ulong, SemaphoreSlim>();
|
||||
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly MethodInfo _sendVoiceStateUpdateMethodInfo;
|
||||
private readonly object _dnetApiClient;
|
||||
private readonly ulong _currentUserId;
|
||||
|
||||
public AyuVoiceStateService(DiscordSocketClient client)
|
||||
{
|
||||
_client = client;
|
||||
_currentUserId = _client.CurrentUser.Id;
|
||||
|
||||
var prop = _client.GetType()
|
||||
.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient");
|
||||
_dnetApiClient = prop.GetValue(_client, null);
|
||||
_sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType().GetMethod("SendVoiceStateUpdateAsync");
|
||||
|
||||
_client.LeftGuild += ClientOnLeftGuild;
|
||||
}
|
||||
|
||||
private Task ClientOnLeftGuild(SocketGuild guild)
|
||||
{
|
||||
if (_voiceProxies.TryRemove(guild.Id, out var proxy))
|
||||
{
|
||||
proxy.StopGateway();
|
||||
proxy.SetGateway(null);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task InvokeSendVoiceStateUpdateAsync(ulong guildId, ulong? channelId = null, bool isDeafened = false, bool isMuted = false)
|
||||
{
|
||||
// return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted);
|
||||
return (Task) _sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient, new object[] {guildId, channelId, isMuted, isDeafened, null});
|
||||
}
|
||||
|
||||
private Task SendLeaveVoiceChannelInternalAsync(ulong guildId)
|
||||
=> InvokeSendVoiceStateUpdateAsync(guildId);
|
||||
|
||||
private Task SendJoinVoiceChannelInternalAsync(ulong guildId, ulong channelId)
|
||||
=> InvokeSendVoiceStateUpdateAsync(guildId, channelId);
|
||||
|
||||
private SemaphoreSlim GetVoiceGatewayLock(ulong guildId) => _voiceGatewayLocks.GetOrAdd(guildId, new SemaphoreSlim(1, 1));
|
||||
|
||||
private async Task LeaveVoiceChannelInternalAsync(ulong guildId)
|
||||
{
|
||||
var complete = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
|
||||
{
|
||||
if (user is SocketGuildUser guildUser
|
||||
&& guildUser.Guild.Id == guildId
|
||||
&& newState.VoiceChannel?.Id is null)
|
||||
{
|
||||
complete.TrySetResult(true);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
|
||||
|
||||
if (_voiceProxies.TryGetValue(guildId, out var proxy))
|
||||
{
|
||||
_ = proxy.StopGateway();
|
||||
proxy.SetGateway(null);
|
||||
}
|
||||
|
||||
await SendLeaveVoiceChannelInternalAsync(guildId);
|
||||
await Task.WhenAny(Task.Delay(1500), complete.Task);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
|
||||
}
|
||||
}
|
||||
public async Task LeaveVoiceChannel(ulong guildId)
|
||||
{
|
||||
var gwLock = GetVoiceGatewayLock(guildId);
|
||||
await gwLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await LeaveVoiceChannelInternalAsync(guildId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
gwLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IVoiceProxy> InternalConnectToVcAsync(ulong guildId, ulong channelId)
|
||||
{
|
||||
var voiceStateUpdatedSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var voiceServerUpdatedSource = new TaskCompletionSource<SocketVoiceServer>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
|
||||
{
|
||||
if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId)
|
||||
{
|
||||
if (newState.VoiceChannel?.Id == channelId)
|
||||
voiceStateUpdatedSource.TrySetResult(newState.VoiceSessionId);
|
||||
|
||||
voiceStateUpdatedSource.TrySetResult(null);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task OnVoiceServerUpdated(SocketVoiceServer data)
|
||||
{
|
||||
if (data.Guild.Id == guildId)
|
||||
{
|
||||
voiceServerUpdatedSource.TrySetResult(data);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_client.VoiceServerUpdated += OnVoiceServerUpdated;
|
||||
_client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
|
||||
|
||||
await SendJoinVoiceChannelInternalAsync(guildId, channelId);
|
||||
|
||||
// create a delay task, how much to wait for gateway response
|
||||
var delayTask = Task.Delay(2500);
|
||||
|
||||
// either delay or successful voiceStateUpdate
|
||||
var maybeUpdateTask = Task.WhenAny(delayTask, voiceStateUpdatedSource.Task);
|
||||
// either delay or successful voiceServerUpdate
|
||||
var maybeServerTask = Task.WhenAny(delayTask, voiceServerUpdatedSource.Task);
|
||||
|
||||
// wait for both to end (max 1s) and check if either of them is a delay task
|
||||
var results = await Task.WhenAll(maybeUpdateTask, maybeServerTask);
|
||||
if (results[0] == delayTask || results[1] == delayTask)
|
||||
{
|
||||
// if either is delay, return null - connection unsuccessful
|
||||
return null;
|
||||
}
|
||||
|
||||
// if both are succesful, that means we can safely get
|
||||
// the values from completion sources
|
||||
|
||||
var session = await voiceStateUpdatedSource.Task;
|
||||
|
||||
// session can be null. Means we disconnected, or connected to the wrong channel (?!)
|
||||
if (session is null)
|
||||
return null;
|
||||
|
||||
var voiceServerData = await voiceServerUpdatedSource.Task;
|
||||
|
||||
VoiceGateway CreateVoiceGatewayLocal() =>
|
||||
new VoiceGateway(
|
||||
guildId,
|
||||
_currentUserId,
|
||||
session,
|
||||
voiceServerData.Token,
|
||||
voiceServerData.Endpoint
|
||||
);
|
||||
|
||||
var current = _voiceProxies.AddOrUpdate(
|
||||
guildId,
|
||||
(gid) => new VoiceProxy(CreateVoiceGatewayLocal()),
|
||||
(gid, currentProxy) =>
|
||||
{
|
||||
_ = currentProxy.StopGateway();
|
||||
currentProxy.SetGateway(CreateVoiceGatewayLocal());
|
||||
return currentProxy;
|
||||
}
|
||||
);
|
||||
|
||||
_ = current.StartGateway(); // don't await, this blocks until gateway is closed
|
||||
return current;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_client.VoiceServerUpdated -= OnVoiceServerUpdated;
|
||||
_client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IVoiceProxy> JoinVoiceChannel(ulong guildId, ulong channelId, bool forceReconnect = true)
|
||||
{
|
||||
var gwLock = GetVoiceGatewayLock(guildId);
|
||||
await gwLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await LeaveVoiceChannelInternalAsync(guildId);
|
||||
return await InternalConnectToVcAsync(guildId, channelId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
gwLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy)
|
||||
=> _voiceProxies.TryGetValue(guildId, out proxy);
|
||||
}
|
||||
}
|
41
NadekoBot.Core/Modules/Music/Services/IMusicService.cs
Normal file
41
NadekoBot.Core/Modules/Music/Services/IMusicService.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using NadekoBot.Core.Common;
|
||||
using NadekoBot.Core.Modules.Music;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Services
|
||||
{
|
||||
public interface IMusicService : IPlaceholderProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Leave voice channel in the specified guild if it's connected to one
|
||||
/// </summary>
|
||||
/// <param name="guildId">Id of the guild</param>
|
||||
public Task LeaveVoiceChannelAsync(ulong guildId);
|
||||
|
||||
/// <summary>
|
||||
/// Joins the voice channel with the specified id
|
||||
/// </summary>
|
||||
/// <param name="guildId">Id of the guild where the voice channel is</param>
|
||||
/// <param name="voiceChannelId">Id of the voice channel</param>
|
||||
public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId);
|
||||
|
||||
Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel);
|
||||
bool TryGetMusicPlayer(ulong guildId, out IMusicPlayer musicPlayer);
|
||||
Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string playlistId, string queuer);
|
||||
Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer);
|
||||
Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer);
|
||||
Task<IUserMessage?> SendToOutputAsync(ulong guildId, EmbedBuilder embed);
|
||||
Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId);
|
||||
Task<IList<(string Title, string Url)>> SearchVideosAsync(string query);
|
||||
Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId);
|
||||
Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType);
|
||||
Task SetVolumeAsync(ulong guildId, int value);
|
||||
Task<bool> ToggleAutoDisconnectAsync(ulong guildId);
|
||||
Task<QualityPreset> GetMusicQualityAsync(ulong guildId);
|
||||
Task SetMusicQualityAsync(ulong guildId, QualityPreset preset);
|
||||
}
|
||||
}
|
457
NadekoBot.Core/Modules/Music/Services/MusicService.cs
Normal file
457
NadekoBot.Core/Modules/Music/Services/MusicService.cs
Normal file
@@ -0,0 +1,457 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Core.Modules.Music;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Core.Services.Database.Repositories.Impl;
|
||||
using NadekoBot.Extensions;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Services
|
||||
{
|
||||
public sealed class MusicService : IMusicService
|
||||
{
|
||||
private readonly AyuVoiceStateService _voiceStateService;
|
||||
private readonly ITrackResolveProvider _trackResolveProvider;
|
||||
private readonly DbService _db;
|
||||
private readonly IYoutubeResolver _ytResolver;
|
||||
private readonly ILocalTrackResolver _localResolver;
|
||||
private readonly ISoundcloudResolver _scResolver;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotStrings _strings;
|
||||
private readonly IGoogleApiService _googleApiService;
|
||||
private readonly YtLoader _ytLoader;
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, IMusicPlayer> _players;
|
||||
private readonly ConcurrentDictionary<ulong, (ITextChannel Default, ITextChannel? Override)> _outputChannels;
|
||||
private readonly ConcurrentDictionary<ulong, MusicPlayerSettings> _settings;
|
||||
|
||||
public MusicService(AyuVoiceStateService voiceStateService, ITrackResolveProvider trackResolveProvider,
|
||||
DbService db, IYoutubeResolver ytResolver, ILocalTrackResolver localResolver, ISoundcloudResolver scResolver,
|
||||
DiscordSocketClient client, IBotStrings strings, IGoogleApiService googleApiService, YtLoader ytLoader)
|
||||
{
|
||||
_voiceStateService = voiceStateService;
|
||||
_trackResolveProvider = trackResolveProvider;
|
||||
_db = db;
|
||||
_ytResolver = ytResolver;
|
||||
_localResolver = localResolver;
|
||||
_scResolver = scResolver;
|
||||
_client = client;
|
||||
_strings = strings;
|
||||
_googleApiService = googleApiService;
|
||||
_ytLoader = ytLoader;
|
||||
|
||||
_players = new ConcurrentDictionary<ulong, IMusicPlayer>();
|
||||
_outputChannels = new ConcurrentDictionary<ulong, (ITextChannel, ITextChannel?)>();
|
||||
_settings = new ConcurrentDictionary<ulong, MusicPlayerSettings>();
|
||||
|
||||
_client.LeftGuild += ClientOnLeftGuild;
|
||||
}
|
||||
|
||||
private void DisposeMusicPlayer(IMusicPlayer musicPlayer)
|
||||
{
|
||||
musicPlayer.Kill();
|
||||
_ = Task.Delay(10_000).ContinueWith(_ => musicPlayer.Dispose());
|
||||
}
|
||||
|
||||
private void RemoveMusicPlayer(ulong guildId)
|
||||
{
|
||||
_outputChannels.TryRemove(guildId, out _);
|
||||
if (_players.TryRemove(guildId, out var mp))
|
||||
{
|
||||
DisposeMusicPlayer(mp);
|
||||
}
|
||||
}
|
||||
|
||||
private Task ClientOnLeftGuild(SocketGuild guild)
|
||||
{
|
||||
RemoveMusicPlayer(guild.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task LeaveVoiceChannelAsync(ulong guildId)
|
||||
{
|
||||
RemoveMusicPlayer(guildId);
|
||||
await _voiceStateService.LeaveVoiceChannel(guildId);
|
||||
}
|
||||
|
||||
public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId)
|
||||
=> _voiceStateService.JoinVoiceChannel(guildId, voiceChannelId);
|
||||
|
||||
public async Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel)
|
||||
{
|
||||
var newPLayer = await CreateMusicPlayerInternalAsync(contextChannel.GuildId, contextChannel);
|
||||
if (newPLayer is null)
|
||||
return null;
|
||||
|
||||
return _players.GetOrAdd(contextChannel.GuildId, newPLayer);
|
||||
}
|
||||
|
||||
public bool TryGetMusicPlayer(ulong guildId, out IMusicPlayer musicPlayer)
|
||||
=> _players.TryGetValue(guildId, out musicPlayer);
|
||||
|
||||
public async Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer)
|
||||
{
|
||||
var count = 0;
|
||||
await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query))
|
||||
{
|
||||
if (mp.IsKilled)
|
||||
break;
|
||||
|
||||
mp.EnqueueTrack(track, queuer);
|
||||
++count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public async Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer)
|
||||
{
|
||||
await foreach (var track in _localResolver.ResolveDirectoryAsync(dirPath))
|
||||
{
|
||||
if (mp.IsKilled)
|
||||
break;
|
||||
|
||||
mp.EnqueueTrack(track, queuer);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer)
|
||||
{
|
||||
var i = 0;
|
||||
await foreach (var track in _scResolver.ResolvePlaylistAsync(playlist))
|
||||
{
|
||||
if (mp.IsKilled)
|
||||
break;
|
||||
|
||||
mp.EnqueueTrack(track, queuer);
|
||||
++i;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
private async Task<IMusicPlayer?> CreateMusicPlayerInternalAsync(ulong guildId, ITextChannel defaultChannel)
|
||||
{
|
||||
var queue = new MusicQueue();
|
||||
var resolver = _trackResolveProvider;
|
||||
|
||||
if (!_voiceStateService.TryGetProxy(guildId, out var proxy))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var settings = await GetSettingsInternalAsync(guildId);
|
||||
|
||||
ITextChannel? overrideChannel = null;
|
||||
if (settings.MusicChannelId is ulong channelId)
|
||||
{
|
||||
overrideChannel = _client.GetGuild(guildId)?.GetTextChannel(channelId);
|
||||
|
||||
if (overrideChannel is null)
|
||||
{
|
||||
Log.Warning("Saved music output channel doesn't exist, falling back to current channel");
|
||||
}
|
||||
}
|
||||
|
||||
_outputChannels[guildId] = (defaultChannel, overrideChannel);
|
||||
|
||||
var mp = new MusicPlayer(
|
||||
queue,
|
||||
resolver,
|
||||
proxy,
|
||||
settings.QualityPreset
|
||||
);
|
||||
|
||||
mp.SetRepeat(settings.PlayerRepeat);
|
||||
|
||||
if (settings.Volume >= 0 && settings.Volume <= 100)
|
||||
{
|
||||
mp.SetVolume(settings.Volume);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("Saved Volume is outside of valid range >= 0 && <=100 ({Volume})", settings.Volume);
|
||||
}
|
||||
|
||||
mp.OnCompleted += OnTrackCompleted(guildId);
|
||||
mp.OnStarted += OnTrackStarted(guildId);
|
||||
mp.OnQueueStopped += OnQueueStopped(guildId);
|
||||
|
||||
return mp;
|
||||
}
|
||||
|
||||
public Task<IUserMessage?> SendToOutputAsync(ulong guildId, EmbedBuilder embed)
|
||||
{
|
||||
if (_outputChannels.TryGetValue(guildId, out var chan))
|
||||
return (chan.Default ?? chan.Override).EmbedAsync(embed);
|
||||
|
||||
return Task.FromResult<IUserMessage?>(null);
|
||||
}
|
||||
|
||||
private Func<IMusicPlayer, IQueuedTrackInfo, Task> OnTrackCompleted(ulong guildId)
|
||||
{
|
||||
IUserMessage? lastFinishedMessage = null;
|
||||
return async (mp, trackInfo) =>
|
||||
{
|
||||
_ = lastFinishedMessage?.DeleteAsync();
|
||||
var embed = new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithAuthor(eab => eab.WithName(GetText(guildId, "finished_song")).WithMusicIcon())
|
||||
.WithDescription(trackInfo.PrettyName())
|
||||
.WithFooter(trackInfo.PrettyTotalTime());
|
||||
|
||||
lastFinishedMessage = await SendToOutputAsync(guildId, embed);
|
||||
};
|
||||
}
|
||||
|
||||
private Func<IMusicPlayer, IQueuedTrackInfo, int, Task> OnTrackStarted(ulong guildId)
|
||||
{
|
||||
IUserMessage? lastPlayingMessage = null;
|
||||
return async (mp, trackInfo, index) =>
|
||||
{
|
||||
_ = lastPlayingMessage?.DeleteAsync();
|
||||
var embed = new EmbedBuilder().WithOkColor()
|
||||
.WithAuthor(eab => eab.WithName(GetText(guildId, "playing_song", index + 1)).WithMusicIcon())
|
||||
.WithDescription(trackInfo.PrettyName())
|
||||
.WithFooter(ef => ef.WithText($"{mp.PrettyVolume()} | {trackInfo.PrettyInfo()}"));
|
||||
|
||||
lastPlayingMessage = await SendToOutputAsync(guildId, embed);
|
||||
};
|
||||
}
|
||||
|
||||
private Func<IMusicPlayer, Task> OnQueueStopped(ulong guildId)
|
||||
=> (mp) =>
|
||||
{
|
||||
if (_settings.TryGetValue(guildId, out var settings))
|
||||
{
|
||||
if (settings.AutoDisconnect)
|
||||
{
|
||||
return LeaveVoiceChannelAsync(guildId);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
// this has to be done because dragging bot to another vc isn't supported yet
|
||||
public async Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId)
|
||||
{
|
||||
if (!TryGetMusicPlayer(guildId, out var mp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mp.IsStopped)
|
||||
{
|
||||
if (!_voiceStateService.TryGetProxy(guildId, out var proxy)
|
||||
|| proxy.State == VoiceProxy.VoiceProxyState.Stopped)
|
||||
{
|
||||
await JoinVoiceChannelAsync(guildId, voiceChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
mp.Next();
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<IList<(string Title, string Url)>> SearchYtLoaderVideosAsync(string query)
|
||||
{
|
||||
var result = await _ytLoader.LoadResultsAsync(query);
|
||||
return result.Select(x => (x.Title, x.Url)).ToList();
|
||||
}
|
||||
|
||||
private async Task<IList<(string Title, string Url)>> SearchGoogleApiVideosAsync(string query)
|
||||
{
|
||||
var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5);
|
||||
return result.Select(x => (x.Name, x.Url)).ToList();
|
||||
}
|
||||
|
||||
public async Task<IList<(string Title, string Url)>> SearchVideosAsync(string query)
|
||||
{
|
||||
try
|
||||
{
|
||||
IList<(string, string)> videos = await SearchYtLoaderVideosAsync(query);
|
||||
if (videos.Count > 0)
|
||||
{
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning("Failed geting videos with YtLoader: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await SearchGoogleApiVideosAsync(query);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning("Failed getting video results with Google Api. " +
|
||||
"Probably google api key missing: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
|
||||
return Array.Empty<(string, string)>();
|
||||
}
|
||||
|
||||
private string GetText(ulong guildId, string key, params object[] args)
|
||||
=> _strings.GetText(key, guildId, args);
|
||||
|
||||
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders()
|
||||
{
|
||||
// random song that's playing
|
||||
yield return ("%music.playing%", () =>
|
||||
{
|
||||
var randomPlayingTrack = _players
|
||||
.Select(x => x.Value.GetCurrentTrack(out _))
|
||||
.Where(x => !(x is null))
|
||||
.Shuffle()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (randomPlayingTrack is null)
|
||||
return "-";
|
||||
|
||||
return randomPlayingTrack.Title;
|
||||
});
|
||||
|
||||
// number of servers currently listening to music
|
||||
yield return ("%music.servers%", () =>
|
||||
{
|
||||
var count = _players
|
||||
.Select(x => x.Value.GetCurrentTrack(out _))
|
||||
.Count(x => !(x is null));
|
||||
|
||||
return count.ToString();
|
||||
});
|
||||
|
||||
yield return ("%music.queued%", () =>
|
||||
{
|
||||
var count = _players
|
||||
.Sum(x => x.Value.GetQueuedTracks().Count);
|
||||
|
||||
return count.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
#region Settings
|
||||
|
||||
private async Task<MusicPlayerSettings> GetSettingsInternalAsync(ulong guildId)
|
||||
{
|
||||
if (_settings.TryGetValue(guildId, out var settings))
|
||||
return settings;
|
||||
|
||||
using var uow = _db.GetDbContext();
|
||||
var toReturn = _settings[guildId] = await uow._context.MusicPlayerSettings.ForGuildAsync(guildId);
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
private async Task ModifySettingsInternalAsync<TState>(
|
||||
ulong guildId,
|
||||
Action<MusicPlayerSettings, TState> action,
|
||||
TState state)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var ms = await uow._context.MusicPlayerSettings.ForGuildAsync(guildId);
|
||||
action(ms, state);
|
||||
await uow.SaveChangesAsync();
|
||||
_settings[guildId] = ms;
|
||||
}
|
||||
|
||||
public async Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId)
|
||||
{
|
||||
if (channelId is null)
|
||||
{
|
||||
await UnsetMusicChannelAsync(guildId);
|
||||
return true;
|
||||
}
|
||||
|
||||
var channel = _client.GetGuild(guildId)?.GetTextChannel(channelId.Value);
|
||||
if (channel is null)
|
||||
return false;
|
||||
|
||||
await ModifySettingsInternalAsync(guildId, (settings, chId) =>
|
||||
{
|
||||
settings.MusicChannelId = chId;
|
||||
}, channelId);
|
||||
|
||||
_outputChannels.AddOrUpdate(guildId,
|
||||
(channel, channel),
|
||||
(key, old) => (old.Default, channel));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task UnsetMusicChannelAsync(ulong guildId)
|
||||
{
|
||||
await ModifySettingsInternalAsync(guildId, (settings, _) =>
|
||||
{
|
||||
settings.MusicChannelId = null;
|
||||
}, (ulong?)null);
|
||||
|
||||
if (_outputChannels.TryGetValue(guildId, out var old))
|
||||
_outputChannels[guildId] = (old.Default, null);
|
||||
}
|
||||
|
||||
public async Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType)
|
||||
{
|
||||
await ModifySettingsInternalAsync(guildId, (settings, type) =>
|
||||
{
|
||||
settings.PlayerRepeat = type;
|
||||
}, repeatType);
|
||||
|
||||
if (TryGetMusicPlayer(guildId, out var mp))
|
||||
mp.SetRepeat(repeatType);
|
||||
}
|
||||
|
||||
public async Task SetVolumeAsync(ulong guildId, int value)
|
||||
{
|
||||
if (value < 0 || value > 100)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
await ModifySettingsInternalAsync(guildId, (settings, newValue) =>
|
||||
{
|
||||
settings.Volume = newValue;
|
||||
}, value);
|
||||
|
||||
if (TryGetMusicPlayer(guildId, out var mp))
|
||||
mp.SetVolume(value);
|
||||
}
|
||||
|
||||
public async Task<bool> ToggleAutoDisconnectAsync(ulong guildId)
|
||||
{
|
||||
var newState = false;
|
||||
await ModifySettingsInternalAsync(guildId, (settings, _) =>
|
||||
{
|
||||
newState = settings.AutoDisconnect = !settings.AutoDisconnect;
|
||||
}, default(object));
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
public async Task<QualityPreset> GetMusicQualityAsync(ulong guildId)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var settings = await uow._context.MusicPlayerSettings.ForGuildAsync(guildId);
|
||||
return settings.QualityPreset;
|
||||
}
|
||||
|
||||
public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset)
|
||||
{
|
||||
return ModifySettingsInternalAsync(guildId, (settings, _) =>
|
||||
{
|
||||
settings.QualityPreset = preset;
|
||||
}, preset);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
297
NadekoBot.Core/Modules/Music/Services/_OldMusicService.cs
Normal file
297
NadekoBot.Core/Modules/Music/Services/_OldMusicService.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
// using System.Collections.Concurrent;
|
||||
// using System.Linq;
|
||||
// using System.Threading.Tasks;
|
||||
// using Discord;
|
||||
// using NadekoBot.Extensions;
|
||||
// using NadekoBot.Core.Services.Database.Models;
|
||||
// using System.IO;
|
||||
// using Discord.Commands;
|
||||
// using Discord.WebSocket;
|
||||
// using NadekoBot.Common;
|
||||
// using NadekoBot.Core.Services.Impl;
|
||||
// using NadekoBot.Core.Services;
|
||||
// using NadekoBot.Modules.Music.Common;
|
||||
// using NadekoBot.Modules.Music.Common.Exceptions;
|
||||
// using NadekoBot.Modules.Music.Common.SongResolver;
|
||||
// using NadekoBot.Common.Collections;
|
||||
// using Microsoft.EntityFrameworkCore;
|
||||
// using Serilog;
|
||||
//
|
||||
// namespace NadekoBot.Modules.Music.Services
|
||||
// {
|
||||
// public class MusicService : INService, IUnloadableService
|
||||
// {
|
||||
// public const string MusicDataPath = "data/musicdata";
|
||||
//
|
||||
// private readonly IGoogleApiService _google;
|
||||
// private readonly IBotStrings _strings;
|
||||
// private readonly DbService _db;
|
||||
// private readonly AyuVoiceStateService _tracker;
|
||||
// private readonly ConcurrentDictionary<ulong, MusicSettings> _musicSettings;
|
||||
// private readonly SoundCloudApiService _sc;
|
||||
// private readonly IBotCredentials _creds;
|
||||
// private readonly ConcurrentDictionary<ulong, float> _defaultVolumes;
|
||||
//
|
||||
// public ConcurrentHashSet<ulong> AutoDcServers { get; }
|
||||
//
|
||||
// private readonly DiscordSocketClient _client;
|
||||
//
|
||||
// public ConcurrentDictionary<ulong, MusicPlayer> MusicPlayers { get; } = new ConcurrentDictionary<ulong, MusicPlayer>();
|
||||
//
|
||||
// public MusicService(DiscordSocketClient client, IGoogleApiService google,
|
||||
// IBotStrings strings, DbService db, AyuVoiceStateService tracker,
|
||||
// SoundCloudApiService sc, IBotCredentials creds, NadekoBot bot)
|
||||
// {
|
||||
// _client = client;
|
||||
// _google = google;
|
||||
// _strings = strings;
|
||||
// _db = db;
|
||||
// _tracker = tracker;
|
||||
// _sc = sc;
|
||||
// _creds = creds;
|
||||
// _musicSettings = bot.AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.MusicSettings)
|
||||
// .ToConcurrent();
|
||||
//
|
||||
// _client.LeftGuild += _client_LeftGuild;
|
||||
// try { Directory.Delete(MusicDataPath, true); } catch { }
|
||||
//
|
||||
// _defaultVolumes = new ConcurrentDictionary<ulong, float>(
|
||||
// bot.AllGuildConfigs
|
||||
// .ToDictionary(x => x.GuildId, x => x.DefaultMusicVolume));
|
||||
//
|
||||
// AutoDcServers = new ConcurrentHashSet<ulong>(bot.AllGuildConfigs.Where(x => x.AutoDcFromVc).Select(x => x.GuildId));
|
||||
//
|
||||
// Directory.CreateDirectory(MusicDataPath);
|
||||
// }
|
||||
//
|
||||
// public Task Unload()
|
||||
// {
|
||||
// _client.LeftGuild -= _client_LeftGuild;
|
||||
// return Task.CompletedTask;
|
||||
// }
|
||||
//
|
||||
// private Task _client_LeftGuild(SocketGuild arg)
|
||||
// {
|
||||
// var t = DestroyPlayer(arg.Id);
|
||||
// return Task.CompletedTask;
|
||||
// }
|
||||
//
|
||||
// public float GetDefaultVolume(ulong guildId)
|
||||
// {
|
||||
// return _defaultVolumes.GetOrAdd(guildId, (id) =>
|
||||
// {
|
||||
// using (var uow = _db.GetDbContext())
|
||||
// {
|
||||
// return uow.GuildConfigs.ForId(guildId, set => set).DefaultMusicVolume;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// public Task<MusicPlayer> GetOrCreatePlayer(ICommandContext context)
|
||||
// {
|
||||
// var gUsr = (IGuildUser)context.User;
|
||||
// var txtCh = (ITextChannel)context.Channel;
|
||||
// var vCh = gUsr.VoiceChannel;
|
||||
// return GetOrCreatePlayer(context.Guild.Id, vCh, txtCh);
|
||||
// }
|
||||
//
|
||||
// public async Task<MusicPlayer> GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh)
|
||||
// {
|
||||
// string GetText(string text, params object[] replacements) =>
|
||||
// _strings.GetText(text, textCh.Guild.Id, replacements);
|
||||
//
|
||||
// if (voiceCh == null || voiceCh.Guild != textCh.Guild)
|
||||
// {
|
||||
// if (textCh != null)
|
||||
// {
|
||||
// await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false);
|
||||
// }
|
||||
// throw new NotInVoiceChannelException();
|
||||
// }
|
||||
// return MusicPlayers.GetOrAdd(guildId, _ =>
|
||||
// {
|
||||
// var vol = GetDefaultVolume(guildId);
|
||||
// if (!_musicSettings.TryGetValue(guildId, out var ms))
|
||||
// ms = new MusicSettings();
|
||||
//
|
||||
// var mp = new MusicPlayer(this, ms, _google, voiceCh, textCh, vol, _tracker);
|
||||
//
|
||||
// IUserMessage playingMessage = null;
|
||||
// IUserMessage lastFinishedMessage = null;
|
||||
//
|
||||
// mp.OnCompleted += async (s, song) =>
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// lastFinishedMessage?.DeleteAfter(0);
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// lastFinishedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
// .WithAuthor(eab => eab.WithName(GetText("finished_song")).WithMusicIcon())
|
||||
// .WithDescription(song.PrettyName)
|
||||
// .WithFooter(ef => ef.WithText(song.PrettyInfo)))
|
||||
// .ConfigureAwait(false);
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
// // ignored
|
||||
// }
|
||||
//
|
||||
// var (Index, Current) = mp.Current;
|
||||
// if (Current == null
|
||||
// && !mp.RepeatCurrentSong
|
||||
// && !mp.RepeatPlaylist
|
||||
// && !mp.FairPlay
|
||||
// && AutoDcServers.Contains(guildId))
|
||||
// {
|
||||
// await DestroyPlayer(guildId).ConfigureAwait(false);
|
||||
// }
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
// // ignored
|
||||
// }
|
||||
// };
|
||||
// mp.OnStarted += async (player, song) =>
|
||||
// {
|
||||
// //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); }
|
||||
// //catch
|
||||
// //{
|
||||
// // // ignored
|
||||
// //}
|
||||
// var sender = player;
|
||||
// if (sender == null)
|
||||
// return;
|
||||
// try
|
||||
// {
|
||||
// playingMessage?.DeleteAfter(0);
|
||||
//
|
||||
// playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
// .WithAuthor(eab => eab.WithName(GetText("playing_song", song.Index + 1)).WithMusicIcon())
|
||||
// .WithDescription(song.Song.PrettyName)
|
||||
// .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.Song.PrettyInfo)))
|
||||
// .ConfigureAwait(false);
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
// // ignored
|
||||
// }
|
||||
// };
|
||||
// mp.OnPauseChanged += async (player, paused) =>
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// IUserMessage msg;
|
||||
// if (paused)
|
||||
// msg = await mp.OutputTextChannel.SendConfirmAsync(GetText("paused")).ConfigureAwait(false);
|
||||
// else
|
||||
// msg = await mp.OutputTextChannel.SendConfirmAsync(GetText("resumed")).ConfigureAwait(false);
|
||||
//
|
||||
// msg?.DeleteAfter(10);
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
// // ignored
|
||||
// }
|
||||
// };
|
||||
// Log.Information("Done creating");
|
||||
// return mp;
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// public MusicPlayer GetPlayerOrDefault(ulong guildId)
|
||||
// {
|
||||
// if (MusicPlayers.TryGetValue(guildId, out var mp))
|
||||
// return mp;
|
||||
// else
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// public async Task TryQueueRelatedSongAsync(SongInfo song, ITextChannel txtCh, IVoiceChannel vch)
|
||||
// {
|
||||
// var related = (await _google.GetRelatedVideosAsync(song.VideoId, 4).ConfigureAwait(false)).ToArray();
|
||||
// if (!related.Any())
|
||||
// return;
|
||||
//
|
||||
// var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.YouTube).ConfigureAwait(false);
|
||||
// if (si == null)
|
||||
// throw new SongNotFoundException();
|
||||
// var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh).ConfigureAwait(false);
|
||||
// mp.Enqueue(si);
|
||||
// }
|
||||
//
|
||||
// public async Task<SongInfo> ResolveSong(string query, string queuerName, MusicType? musicType = null)
|
||||
// {
|
||||
// query.ThrowIfNull(nameof(query));
|
||||
//
|
||||
// ISongResolverFactory resolverFactory = new SongResolverFactory(_sc);
|
||||
// var strategy = await resolverFactory.GetResolveStrategy(query, musicType).ConfigureAwait(false);
|
||||
// var sinfo = await strategy.ResolveSong(query).ConfigureAwait(false);
|
||||
//
|
||||
// if (sinfo == null)
|
||||
// return null;
|
||||
//
|
||||
// sinfo.QueuerName = queuerName;
|
||||
//
|
||||
// return sinfo;
|
||||
// }
|
||||
//
|
||||
// public async Task DestroyAllPlayers()
|
||||
// {
|
||||
// foreach (var key in MusicPlayers.Keys)
|
||||
// {
|
||||
// await DestroyPlayer(key).ConfigureAwait(false);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public async Task DestroyPlayer(ulong id)
|
||||
// {
|
||||
// if (MusicPlayers.TryRemove(id, out var mp))
|
||||
// await mp.Destroy().ConfigureAwait(false);
|
||||
// }
|
||||
//
|
||||
// public bool ToggleAutoDc(ulong id)
|
||||
// {
|
||||
// bool val;
|
||||
// using (var uow = _db.GetDbContext())
|
||||
// {
|
||||
// var gc = uow.GuildConfigs.ForId(id, set => set);
|
||||
// val = gc.AutoDcFromVc = !gc.AutoDcFromVc;
|
||||
// uow.SaveChanges();
|
||||
// }
|
||||
//
|
||||
// if (val)
|
||||
// AutoDcServers.Add(id);
|
||||
// else
|
||||
// AutoDcServers.TryRemove(id);
|
||||
//
|
||||
// return val;
|
||||
// }
|
||||
//
|
||||
// public void UpdateSettings(ulong id, MusicSettings musicSettings)
|
||||
// {
|
||||
// _musicSettings.AddOrUpdate(id, musicSettings, delegate { return musicSettings; });
|
||||
// }
|
||||
//
|
||||
// public void SetMusicChannel(ulong guildId, ulong? cid)
|
||||
// {
|
||||
// using (var uow = _db.GetDbContext())
|
||||
// {
|
||||
// var ms = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.MusicSettings)).MusicSettings;
|
||||
// ms.MusicChannelId = cid;
|
||||
// uow.SaveChanges();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public void SetSongAutoDelete(ulong guildId, bool val)
|
||||
// {
|
||||
// using (var uow = _db.GetDbContext())
|
||||
// {
|
||||
// var ms = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.MusicSettings)).MusicSettings;
|
||||
// ms.SongAutoDelete = val;
|
||||
// uow.SaveChanges();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
75
NadekoBot.Core/Modules/Music/Services/extractor/Misc.cs
Normal file
75
NadekoBot.Core/Modules/Music/Services/extractor/Misc.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Services
|
||||
{
|
||||
|
||||
public sealed partial class YtLoader
|
||||
{
|
||||
|
||||
public class InitRange
|
||||
{
|
||||
public string Start { get; set; }
|
||||
public string End { get; set; }
|
||||
}
|
||||
|
||||
public class IndexRange
|
||||
{
|
||||
public string Start { get; set; }
|
||||
public string End { get; set; }
|
||||
}
|
||||
|
||||
public class ColorInfo
|
||||
{
|
||||
public string Primaries { get; set; }
|
||||
public string TransferCharacteristics { get; set; }
|
||||
public string MatrixCoefficients { get; set; }
|
||||
}
|
||||
|
||||
public class YtAdaptiveFormat
|
||||
{
|
||||
public int Itag { get; set; }
|
||||
public string MimeType { get; set; }
|
||||
public int Bitrate { get; set; }
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public InitRange InitRange { get; set; }
|
||||
public IndexRange IndexRange { get; set; }
|
||||
public string LastModified { get; set; }
|
||||
public string ContentLength { get; set; }
|
||||
public string Quality { get; set; }
|
||||
public int Fps { get; set; }
|
||||
public string QualityLabel { get; set; }
|
||||
public string ProjectionType { get; set; }
|
||||
public int AverageBitrate { get; set; }
|
||||
public ColorInfo ColorInfo { get; set; }
|
||||
public string ApproxDurationMs { get; set; }
|
||||
public string SignatureCipher { get; set; }
|
||||
}
|
||||
|
||||
public abstract class TrackInfo
|
||||
{
|
||||
public abstract string Url { get; }
|
||||
public abstract string Title { get; }
|
||||
public abstract TimeSpan Duration { get; }
|
||||
}
|
||||
|
||||
public sealed class YtTrackInfo : TrackInfo
|
||||
{
|
||||
private const string BaseYoutubeUrl = "https://youtube.com/watch?v=";
|
||||
public override string Url { get; }
|
||||
public override string Title { get; }
|
||||
public override TimeSpan Duration { get; }
|
||||
|
||||
private readonly string _videoId;
|
||||
|
||||
public YtTrackInfo(string title, string videoId, TimeSpan duration)
|
||||
{
|
||||
Title = title;
|
||||
Url = BaseYoutubeUrl + videoId;
|
||||
Duration = duration;
|
||||
|
||||
_videoId = videoId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
140
NadekoBot.Core/Modules/Music/Services/extractor/YtLoader.cs
Normal file
140
NadekoBot.Core/Modules/Music/Services/extractor/YtLoader.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Discord.Net;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Music.Services
|
||||
{
|
||||
public sealed partial class YtLoader
|
||||
{
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private static readonly byte[] YT_RESULT_INITIAL_DATA = Encoding.UTF8.GetBytes("var ytInitialData = ");
|
||||
private static readonly byte[] YT_RESULT_JSON_END = Encoding.UTF8.GetBytes(";<");
|
||||
|
||||
private static readonly string[] durationFormats = new[]
|
||||
{
|
||||
@"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss", @"hhh\:mm\:ss"
|
||||
};
|
||||
|
||||
public YtLoader(IHttpClientFactory httpFactory)
|
||||
{
|
||||
_httpFactory = httpFactory;
|
||||
}
|
||||
|
||||
// public async Task<TrackInfo> LoadTrackByIdAsync(string videoId)
|
||||
// {
|
||||
// using var http = new HttpClient();
|
||||
// http.DefaultRequestHeaders.Add("X-YouTube-Client-Name", "1");
|
||||
// http.DefaultRequestHeaders.Add("X-YouTube-Client-Version", "2.20210520.09.00");
|
||||
// http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
|
||||
//
|
||||
// var responseString = await http.GetStringAsync($"https://youtube.com?" +
|
||||
// $"pbj=1" +
|
||||
// $"&hl=en" +
|
||||
// $"&v=" + videoId);
|
||||
//
|
||||
// var jsonDoc = JsonDocument.Parse(responseString).RootElement;
|
||||
// var elem = jsonDoc.EnumerateArray()
|
||||
// .FirstOrDefault(x => x.TryGetProperty("page", out var elem) && elem.GetString() == "watch");
|
||||
//
|
||||
// var formatsJsonArray = elem.GetProperty("streamingdata")
|
||||
// .GetProperty("formats")
|
||||
// .GetRawText();
|
||||
//
|
||||
// var formats = JsonSerializer.Deserialize<List<YtAdaptiveFormat>>(formatsJsonArray);
|
||||
// var result = formats
|
||||
// .Where(x => x.MimeType.StartsWith("audio/"))
|
||||
// .OrderByDescending(x => x.Bitrate)
|
||||
// .FirstOrDefault();
|
||||
//
|
||||
// if (result is null)
|
||||
// return null;
|
||||
//
|
||||
// return new YtTrackInfo("1", "2", TimeSpan.Zero);
|
||||
// }
|
||||
|
||||
public async Task<IList<TrackInfo>> LoadResultsAsync(string query)
|
||||
{
|
||||
query = Uri.EscapeDataString(query);
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
|
||||
|
||||
byte[] response;
|
||||
try
|
||||
{
|
||||
response = await http.GetByteArrayAsync($"https://youtube.com/results?hl=en&search_query={query}");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Log.Warning("Unable to retrieve data with YtLoader: {ErrorMessage}", ex.Message);
|
||||
return null;
|
||||
}
|
||||
|
||||
// there is a lot of useless html above the script tag, however if html gets significantly reduced
|
||||
// this will result in the json being cut off
|
||||
|
||||
var mem = GetScriptResponseSpan(response);
|
||||
var root = JsonDocument.Parse(mem).RootElement;
|
||||
|
||||
var tracksJsonItems = root
|
||||
.GetProperty("contents")
|
||||
.GetProperty("twoColumnSearchResultsRenderer")
|
||||
.GetProperty("primaryContents")
|
||||
.GetProperty("sectionListRenderer")
|
||||
.GetProperty("contents")
|
||||
[0]
|
||||
.GetProperty("itemSectionRenderer")
|
||||
.GetProperty("contents")
|
||||
.EnumerateArray();
|
||||
|
||||
var tracks = new List<TrackInfo>();
|
||||
foreach (var track in tracksJsonItems)
|
||||
{
|
||||
if(!track.TryGetProperty("videoRenderer", out var elem))
|
||||
continue;
|
||||
|
||||
var videoId = elem.GetProperty("videoId").GetString();
|
||||
// var thumb = elem.GetProperty("thumbnail").GetProperty("thumbnails")[0].GetProperty("url").GetString();
|
||||
var title = elem.GetProperty("title").GetProperty("runs")[0].GetProperty("text").GetString();
|
||||
var durationString = elem.GetProperty("lengthText").GetProperty("simpleText").GetString();
|
||||
|
||||
if (!TimeSpan.TryParseExact(durationString, durationFormats, CultureInfo.InvariantCulture,
|
||||
out var duration))
|
||||
{
|
||||
Log.Warning("Cannot parse duration: {DurationString}", durationString);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracks.Add(new YtTrackInfo(title, videoId, duration));
|
||||
if (tracks.Count >= 5)
|
||||
break;
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
private Memory<byte> GetScriptResponseSpan(byte[] response)
|
||||
{
|
||||
var responseSpan = response.AsSpan().Slice(140_000);
|
||||
var startIndex = responseSpan.IndexOf(YT_RESULT_INITIAL_DATA);
|
||||
if (startIndex == -1)
|
||||
return null; // todo try selecting html
|
||||
startIndex += YT_RESULT_INITIAL_DATA.Length;
|
||||
|
||||
var endIndex = 140_000 + startIndex + responseSpan.Slice(startIndex + 20_000).IndexOf(YT_RESULT_JSON_END) + 20_000;
|
||||
startIndex += 140_000;
|
||||
return response.AsMemory(
|
||||
startIndex,
|
||||
endIndex - startIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user