using NadekoBot.Db; using NadekoBot.Services.Database.Models; using System.Diagnostics.CodeAnalysis; using Nadeko.Common; 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 IEmbedBuilderService _eb; private readonly ConcurrentDictionary _players; private readonly ConcurrentDictionary _outputChannels; private readonly ConcurrentDictionary _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(); _outputChannels = new ConcurrentDictionary(); _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 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 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 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 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, 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 SendToOutputAsync(ulong guildId, IEmbedBuilder embed) { if (_outputChannels.TryGetValue(guildId, out var chan)) { var msg = await (chan.Override ?? chan.Default).EmbedAsync(embed); return msg; } return null; } private Func OnTrackCompleted(ulong guildId) { IUserMessage? lastFinishedMessage = null; return async (mp, trackInfo) => { _ = lastFinishedMessage?.DeleteAsync(); var embed = _eb.Create() .WithOkColor() .WithAuthor(GetText(guildId, strs.finished_track), Music.MUSIC_ICON_URL) .WithDescription(trackInfo.PrettyName()) .WithFooter(trackInfo.PrettyTotalTime()); lastFinishedMessage = await SendToOutputAsync(guildId, embed); }; } private Func OnTrackStarted(ulong guildId) { IUserMessage? lastPlayingMessage = null; return async (mp, trackInfo, index) => { _ = lastPlayingMessage?.DeleteAsync(); var embed = _eb.Create() .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 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 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> SearchYtLoaderVideosAsync(string query) { var result = await _ytLoader.LoadResultsAsync(query); return result.Select(x => (x.Title, x.Url)).ToList(); } private async Task> SearchGoogleApiVideosAsync(string query) { var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5); return result.Select(x => (x.Name, x.Url)).ToList(); } public async Task> 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 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 GetSettingsInternalAsync(ulong guildId) { if (_settings.TryGetValue(guildId, out var settings)) return settings; await using var uow = _db.GetDbContext(); var toReturn = _settings[guildId] = await uow.MusicPlayerSettings.ForGuildAsync(guildId); await uow.SaveChangesAsync(); return toReturn; } private async Task ModifySettingsInternalAsync( ulong guildId, Action action, TState state) { await using var uow = _db.GetDbContext(); var ms = await uow.MusicPlayerSettings.ForGuildAsync(guildId); action(ms, state); await uow.SaveChangesAsync(); _settings[guildId] = ms; } public async Task 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 ToggleAutoDisconnectAsync(ulong guildId) { var newState = false; await ModifySettingsInternalAsync(guildId, (settings, _) => { newState = settings.AutoDisconnect = !settings.AutoDisconnect; }, default(object)); return newState; } public async Task GetMusicQualityAsync(ulong guildId) { await using var uow = _db.GetDbContext(); var settings = await uow.MusicPlayerSettings.ForGuildAsync(guildId); return settings.QualityPreset; } public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset) => ModifySettingsInternalAsync(guildId, (settings, _) => { settings.QualityPreset = preset; }, preset); public async Task 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; } #endregion }