This repository has been archived on 2024-12-22. You can view files and clone it, but cannot push or open issues or pull requests.
elliebot/src/EllieBot/Modules/Music/Services/MusicService.cs

453 lines
No EOL
15 KiB
C#

using EllieBot.Db.Models;
using EllieBot.Modules.Music.Resolvers;
using System.Diagnostics.CodeAnalysis;
namespace EllieBot.Modules.Music.Services;
public sealed class MusicService : IMusicService, IPlaceholderProvider
{
private readonly AyuVoiceStateService _voiceStateService;
private readonly ITrackResolveProvider _trackResolveProvider;
private readonly DbService _db;
private readonly IYoutubeResolverFactory _ytResolver;
private readonly ILocalTrackResolver _localResolver;
private readonly DiscordSocketClient _client;
private readonly IBotStrings _strings;
private readonly IGoogleApiService _googleApiService;
private readonly YtLoader _ytLoader;
private readonly IMessageSenderService _sender;
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,
IYoutubeResolverFactory ytResolver,
ILocalTrackResolver localResolver,
DiscordSocketClient client,
IBotStrings strings,
IGoogleApiService googleApiService,
YtLoader ytLoader,
IMessageSenderService sender)
{
_voiceStateService = voiceStateService;
_trackResolveProvider = trackResolveProvider;
_db = db;
_ytResolver = ytResolver;
_localResolver = localResolver;
_client = client;
_strings = strings;
_googleApiService = googleApiService;
_ytLoader = ytLoader;
_sender = sender;
_players = new();
_outputChannels = new ConcurrentDictionary<ulong, (ITextChannel, ITextChannel?)>();
_settings = new();
_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, [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.GetYoutubeResolver().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);
}
}
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 { } 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,
_ytResolver,
proxy,
_googleApiService,
settings.QualityPreset,
settings.AutoPlay);
mp.SetRepeat(settings.PlayerRepeat);
if (settings.Volume is >= 0 and <= 100)
mp.SetVolume(settings.Volume);
else
Log.Error("Saved Volume is outside of valid range >= 0 && <=100 ({Volume})", settings.Volume);
mp.OnCompleted += OnTrackCompleted(guildId);
mp.OnStarted += OnTrackStarted(guildId);
mp.OnQueueStopped += OnQueueStopped(guildId);
return mp;
}
public async Task<IUserMessage?> SendToOutputAsync(ulong guildId, EmbedBuilder embed)
{
if (_outputChannels.TryGetValue(guildId, out var chan))
{
var msg = await _sender.Response(chan.Override ?? chan.Default)
.Embed(embed)
.SendAsync();
return msg;
}
return null;
}
private Func<IMusicPlayer, IQueuedTrackInfo, Task> OnTrackCompleted(ulong guildId)
{
IUserMessage? lastFinishedMessage = null;
return async (mp, trackInfo) =>
{
_ = lastFinishedMessage?.DeleteAsync();
var embed = _sender.CreateEmbed(guildId)
.WithOkColor()
.WithAuthor(GetText(guildId, strs.finished_track), Music.MUSIC_ICON_URL)
.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 = _sender.CreateEmbed(guildId)
.WithOkColor()
.WithAuthor(GetText(guildId, strs.playing_track(index + 1)), Music.MUSIC_ICON_URL)
.WithDescription(trackInfo.PrettyName())
.WithFooter($"{mp.PrettyVolume()} | {trackInfo.PrettyInfo()}");
lastPlayingMessage = await SendToOutputAsync(guildId, embed);
};
}
private Func<IMusicPlayer, Task> OnQueueStopped(ulong guildId)
=> _ =>
{
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, string Thumb)>> SearchYtLoaderVideosAsync(string query)
{
var result = await _ytLoader.LoadResultsAsync(query);
return result.Select(x => (x.Title, x.Url, x.Thumb)).ToList();
}
private async Task<IList<(string Title, string Url, string Thumb)>> SearchGoogleApiVideosAsync(string query)
{
var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5);
return result.Select(x => (x.Name, x.Url, x.Thumbnail)).ToList();
}
public async Task<IList<(string Title, string Url, string Thumbnail)>> SearchVideosAsync(string query)
{
try
{
IList<(string, 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, string)>();
}
private string GetText(ulong guildId, LocStr str)
=> _strings.GetText(str, guildId);
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders()
{
// random track 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;
await using var uow = _db.GetDbContext();
var toReturn = _settings[guildId] = await uow.Set<MusicPlayerSettings>().ForGuildAsync(guildId);
await uow.SaveChangesAsync();
return toReturn;
}
private async Task ModifySettingsInternalAsync<TState>(
ulong guildId,
Action<MusicPlayerSettings, TState> action,
TState state)
{
await using var uow = _db.GetDbContext();
var ms = await uow.Set<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), (_, 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 is < 0 or > 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)
{
await using var uow = _db.GetDbContext();
var settings = await uow.Set<MusicPlayerSettings>().ForGuildAsync(guildId);
return settings.QualityPreset;
}
public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset)
=> ModifySettingsInternalAsync(guildId,
(settings, _) => { settings.QualityPreset = preset; },
preset);
public async Task<bool> ToggleQueueAutoPlayAsync(ulong guildId)
{
var newValue = false;
await ModifySettingsInternalAsync(guildId,
(settings, _) => newValue = settings.AutoPlay = !settings.AutoPlay,
false);
if (TryGetMusicPlayer(guildId, out var mp))
mp.AutoPlay = newValue;
return newValue;
}
public Task<bool> FairplayAsync(ulong guildId)
{
if (TryGetMusicPlayer(guildId, out var mp))
{
mp.SetFairplay();
return Task.FromResult(true);
}
return Task.FromResult(false);
}
public async Task<IQueuedTrackInfo?> RemoveLastQueuedTrackAsync(ulong guildId)
{
if (TryGetMusicPlayer(guildId, out var mp))
{
var last = await mp.RemoveLastQueuedTrack();
return last;
}
return null;
}
#endregion
}