using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; using Discord; using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; using NLog; using System.IO; using System.Collections.Generic; using Discord.Commands; using Discord.WebSocket; using NadekoBot.Common; using NadekoBot.Services.Impl; using NadekoBot.Services; using NadekoBot.Modules.Music.Common; using NadekoBot.Modules.Music.Common.Exceptions; using NadekoBot.Modules.Music.Common.SongResolver; namespace NadekoBot.Modules.Music.Services { public class MusicService : INService, IUnloadableService { public const string MusicDataPath = "data/musicdata"; private readonly IGoogleApiService _google; private readonly NadekoStrings _strings; private readonly ILocalization _localization; private readonly DbService _db; private readonly Logger _log; private readonly SoundCloudApiService _sc; private readonly IBotCredentials _creds; private readonly ConcurrentDictionary _defaultVolumes; private readonly DiscordSocketClient _client; public ConcurrentDictionary MusicPlayers { get; } = new ConcurrentDictionary(); public MusicService(DiscordSocketClient client, IGoogleApiService google, NadekoStrings strings, ILocalization localization, DbService db, SoundCloudApiService sc, IBotCredentials creds, IEnumerable gcs) { _client = client; _google = google; _strings = strings; _localization = localization; _db = db; _sc = sc; _creds = creds; _log = LogManager.GetCurrentClassLogger(); _client.LeftGuild += _client_LeftGuild; try { Directory.Delete(MusicDataPath, true); } catch { } _defaultVolumes = new ConcurrentDictionary(gcs.ToDictionary(x => x.GuildId, x => x.DefaultMusicVolume)); 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.UnitOfWork) { return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume; } }); } public Task 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 GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh) { string GetText(string text, params object[] replacements) => _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); _log.Info("Checks"); if (voiceCh == null || voiceCh.Guild != textCh.Guild) { if (textCh != null) { await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false); } throw new NotInVoiceChannelException(); } _log.Info("Get or add"); return MusicPlayers.GetOrAdd(guildId, _ => { _log.Info("Getting default volume"); var vol = GetDefaultVolume(guildId); _log.Info("Creating musicplayer instance"); var mp = new MusicPlayer(this, _google, voiceCh, textCh, vol); IUserMessage playingMessage = null; IUserMessage lastFinishedMessage = null; _log.Info("Subscribing"); 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 } } 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.Info("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)).ToArray(); if (!related.Any()) return; var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.YouTube); if (si == null) throw new SongNotFoundException(); var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh); mp.Enqueue(si); } public async Task ResolveSong(string query, string queuerName, MusicType? musicType = null) { query.ThrowIfNull(nameof(query)); ISongResolverFactory resolverFactory = new SongResolverFactory(_sc); var strategy = await resolverFactory.GetResolveStrategy(query, musicType); var sinfo = await strategy.ResolveSong(query); if (sinfo == null) return null; sinfo.QueuerName = queuerName; return sinfo; } public async Task DestroyAllPlayers() { foreach (var key in MusicPlayers.Keys) { await DestroyPlayer(key); } } public async Task DestroyPlayer(ulong id) { if (MusicPlayers.TryRemove(id, out var mp)) await mp.Destroy(); } } }