From 0466701e28f7a727b1333e0cad2d12f6d037f142 Mon Sep 17 00:00:00 2001 From: Toastie Date: Wed, 26 Jun 2024 23:51:35 +1200 Subject: [PATCH] Updated Searches module --- .../Modules/Searches/Crypto/CryptoService.cs | 2 +- .../Crypto/DefaultStockDataService.cs | 43 +-- .../Modules/Searches/Feeds/FeedCommands.cs | 16 +- .../Modules/Searches/Feeds/FeedsService.cs | 76 +++-- .../Modules/Searches/PathOfExileCommands.cs | 312 ------------------ src/EllieBot/Modules/Searches/Searches.cs | 3 +- .../Modules/Searches/SearchesService.cs | 4 +- .../Searches/_common/Config/SearchesConfig.cs | 4 +- 8 files changed, 77 insertions(+), 383 deletions(-) delete mode 100644 src/EllieBot/Modules/Searches/PathOfExileCommands.cs diff --git a/src/EllieBot/Modules/Searches/Crypto/CryptoService.cs b/src/EllieBot/Modules/Searches/Crypto/CryptoService.cs index 146dac3..a28f96e 100644 --- a/src/EllieBot/Modules/Searches/Crypto/CryptoService.cs +++ b/src/EllieBot/Modules/Searches/Crypto/CryptoService.cs @@ -130,7 +130,7 @@ public class CryptoService : IEService await _getCryptoLock.WaitAsync(); try { - var data = await _cache.GetOrAddAsync(new("ellie:crypto_data"), + var data = await _cache.GetOrAddAsync(new("nadeko:crypto_data"), async () => { try diff --git a/src/EllieBot/Modules/Searches/Crypto/DefaultStockDataService.cs b/src/EllieBot/Modules/Searches/Crypto/DefaultStockDataService.cs index 5b5bf40..feb0548 100644 --- a/src/EllieBot/Modules/Searches/Crypto/DefaultStockDataService.cs +++ b/src/EllieBot/Modules/Searches/Crypto/DefaultStockDataService.cs @@ -1,6 +1,7 @@ using AngleSharp; using CsvHelper; using CsvHelper.Configuration; +using System.Diagnostics; using System.Globalization; using System.Text.Json; @@ -22,46 +23,32 @@ public sealed class DefaultStockDataService : IStockDataService, IEService using var http = _httpClientFactory.CreateClient(); - - var quoteHtmlPage = $"https://finance.yahoo.com/quote/{query.ToUpperInvariant()}"; var config = Configuration.Default.WithDefaultLoader(); using var document = await BrowsingContext.New(config).OpenAsync(quoteHtmlPage); - var divElem = - document.QuerySelector( - "#quote-header-info > div:nth-child(2) > div > div > h1"); - var tickerName = (divElem)?.TextContent; + var tickerName = document.QuerySelector("div.top > .left > .container > h1") + ?.TextContent; + + if (tickerName is null) + return default; + var marketcap = document - .QuerySelectorAll("table") - .Skip(1) - .First() - .QuerySelector("tbody > tr > td:nth-child(2)") + .QuerySelector("li > span > fin-streamer[data-field='marketCap']") ?.TextContent; - var volume = document.QuerySelector("td[data-test='AVERAGE_VOLUME_3MONTH-value']") + var volume = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketVolume']") ?.TextContent; - - var close= document.QuerySelector("td[data-test='PREV_CLOSE-value']") - ?.TextContent ?? "0"; - - var price = document - .QuerySelector("#quote-header-info") - ?.QuerySelector("fin-streamer[data-field='regularMarketPrice']") - ?.TextContent ?? close; - - // var data = await http.GetFromJsonAsync( - // $"https://query1.finance.yahoo.com/v7/finance/quote?symbols={query}"); - // - // if (data is null) - // return default; - // var symbol = data.QuoteResponse.Result.FirstOrDefault(); + var close = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketPreviousClose']") + ?.TextContent + ?? "0"; - // if (symbol is null) - // return default; + var price = document.QuerySelector("fin-streamer.livePrice > span") + ?.TextContent + ?? "0"; return new() { diff --git a/src/EllieBot/Modules/Searches/Feeds/FeedCommands.cs b/src/EllieBot/Modules/Searches/Feeds/FeedCommands.cs index 37376f0..316fc73 100644 --- a/src/EllieBot/Modules/Searches/Feeds/FeedCommands.cs +++ b/src/EllieBot/Modules/Searches/Feeds/FeedCommands.cs @@ -1,5 +1,4 @@ -#nullable disable -using CodeHollow.FeedReader; +using CodeHollow.FeedReader; using EllieBot.Modules.Searches.Services; using System.Text.RegularExpressions; @@ -17,19 +16,21 @@ public partial class Searches [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] [Priority(1)] - public Task YtUploadNotif(string url, [Leftover] string message = null) + public Task YtUploadNotif(string url, [Leftover] string? message = null) => YtUploadNotif(url, null, message); [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] [Priority(2)] - public Task YtUploadNotif(string url, ITextChannel channel = null, [Leftover] string message = null) + public Task YtUploadNotif(string url, ITextChannel? channel = null, [Leftover] string? message = null) { var m = _ytChannelRegex.Match(url); if (!m.Success) return Response().Error(strs.invalid_input).SendAsync(); + channel ??= ctx.Channel as ITextChannel; + if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone) message = message?.SanitizeAllMentions(); @@ -42,7 +43,7 @@ public partial class Searches [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] [Priority(0)] - public Task Feed(string url, [Leftover] string message = null) + public Task Feed(string url, [Leftover] string? message = null) => Feed(url, null, message); @@ -50,7 +51,7 @@ public partial class Searches [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] [Priority(1)] - public async Task Feed(string url, ITextChannel channel = null, [Leftover] string message = null) + public async Task Feed(string url, ITextChannel? channel = null, [Leftover] string? message = null) { if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) @@ -59,10 +60,11 @@ public partial class Searches return; } + channel ??= (ITextChannel)ctx.Channel; + if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone) message = message?.SanitizeAllMentions(); - channel ??= (ITextChannel)ctx.Channel; try { await FeedReader.ReadAsync(url); diff --git a/src/EllieBot/Modules/Searches/Feeds/FeedsService.cs b/src/EllieBot/Modules/Searches/Feeds/FeedsService.cs index d19195c..43ad71f 100644 --- a/src/EllieBot/Modules/Searches/Feeds/FeedsService.cs +++ b/src/EllieBot/Modules/Searches/Feeds/FeedsService.cs @@ -79,6 +79,15 @@ public class FeedsService : IEService } } + private DateTime? GetPubDate(FeedItem item) + { + if (item.PublishingDate is not null) + return item.PublishingDate; + if (item.SpecificItem is AtomFeedItem atomItem) + return atomItem.UpdatedDate; + return null; + } + public async Task TrackFeeds() { while (true) @@ -94,24 +103,32 @@ public class FeedsService : IEService { var feed = await FeedReader.ReadAsync(rssUrl); - var items = feed - .Items.Select(item => (Item: item, - LastUpdate: item.PublishingDate?.ToUniversalTime() - ?? (item.SpecificItem as AtomFeedItem)?.UpdatedDate?.ToUniversalTime())) - .Where(data => data.LastUpdate is not null) - .Select(data => (data.Item, LastUpdate: (DateTime)data.LastUpdate)) - .OrderByDescending(data => data.LastUpdate) - .Reverse() // start from the oldest - .ToList(); + var items = new List<(FeedItem Item, DateTime LastUpdate)>(); + foreach (var item in feed.Items) + { + var pubDate = GetPubDate(item); + + if (pubDate is null) + continue; + + items.Add((item, pubDate.Value.ToUniversalTime())); + + // show at most 3 items if you're behind + if (items.Count > 2) + break; + } + + if (items.Count == 0) + continue; if (!_lastPosts.TryGetValue(kvp.Key, out var lastFeedUpdate)) { - lastFeedUpdate = _lastPosts[kvp.Key] = - items.Any() ? items[items.Count - 1].LastUpdate : DateTime.UtcNow; + lastFeedUpdate = _lastPosts[kvp.Key] = items[0].LastUpdate; } - foreach (var (feedItem, itemUpdateDate) in items) + for (var index = 1; index <= items.Count; index++) { + var (feedItem, itemUpdateDate) = items[^index]; if (itemUpdateDate <= lastFeedUpdate) continue; @@ -168,27 +185,26 @@ public class FeedsService : IEService if (!string.IsNullOrWhiteSpace(feedItem.Description)) embed.WithDescription(desc.TrimTo(2048)); - //send the created embed to all subscribed channels - var feedSendTasks = kvp.Value - .Where(x => x.GuildConfig is not null) - .Select(x => - { - var ch = _client.GetGuild(x.GuildConfig.GuildId) - ?.GetTextChannel(x.ChannelId); - if (ch is null) - return null; + var tasks = new List(); - return _sender.Response(ch) - .Embed(embed) - .Text(string.IsNullOrWhiteSpace(x.Message) - ? string.Empty - : x.Message) - .SendAsync(); - }) - .Where(x => x is not null); + foreach (var val in kvp.Value) + { + var ch = _client.GetGuild(val.GuildConfig.GuildId).GetTextChannel(val.ChannelId); - allSendTasks.Add(feedSendTasks.WhenAll()); + if (ch is null) + continue; + + var sendTask = _sender.Response(ch) + .Embed(embed) + .Text(string.IsNullOrWhiteSpace(val.Message) + ? string.Empty + : val.Message) + .SendAsync(); + tasks.Add(sendTask); + } + + allSendTasks.Add(tasks.WhenAll()); // as data retrieval was successful, reset error counter ClearErrors(rssUrl); diff --git a/src/EllieBot/Modules/Searches/PathOfExileCommands.cs b/src/EllieBot/Modules/Searches/PathOfExileCommands.cs deleted file mode 100644 index a966a5b..0000000 --- a/src/EllieBot/Modules/Searches/PathOfExileCommands.cs +++ /dev/null @@ -1,312 +0,0 @@ -#nullable disable -using EllieBot.Modules.Searches.Common; -using EllieBot.Modules.Searches.Services; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Globalization; -using System.Text; - -namespace EllieBot.Modules.Searches; - -public partial class Searches -{ - [Group] - public partial class PathOfExileCommands : EllieModule - { - private const string POE_URL = "https://www.pathofexile.com/character-window/get-characters?accountName="; - private const string PON_URL = "http://poe.ninja/api/Data/GetCurrencyOverview?league="; - private const string POGS_URL = "http://pathofexile.gamepedia.com/api.php?action=opensearch&search="; - - private const string POG_URL = - "https://pathofexile.gamepedia.com/api.php?action=browsebysubject&format=json&subject="; - - private const string POGI_URL = - "https://pathofexile.gamepedia.com/api.php?action=query&prop=imageinfo&iiprop=url&format=json&titles=File:"; - - private const string PROFILE_URL = "https://www.pathofexile.com/account/view-profile/"; - - private readonly IHttpClientFactory _httpFactory; - - private Dictionary currencyDictionary = new(StringComparer.OrdinalIgnoreCase) - { - { "Chaos Orb", "Chaos Orb" }, - { "Orb of Alchemy", "Orb of Alchemy" }, - { "Jeweller's Orb", "Jeweller's Orb" }, - { "Exalted Orb", "Exalted Orb" }, - { "Mirror of Kalandra", "Mirror of Kalandra" }, - { "Vaal Orb", "Vaal Orb" }, - { "Orb of Alteration", "Orb of Alteration" }, - { "Orb of Scouring", "Orb of Scouring" }, - { "Divine Orb", "Divine Orb" }, - { "Orb of Annulment", "Orb of Annulment" }, - { "Master Cartographer's Sextant", "Master Cartographer's Sextant" }, - { "Journeyman Cartographer's Sextant", "Journeyman Cartographer's Sextant" }, - { "Apprentice Cartographer's Sextant", "Apprentice Cartographer's Sextant" }, - { "Blessed Orb", "Blessed Orb" }, - { "Orb of Regret", "Orb of Regret" }, - { "Gemcutter's Prism", "Gemcutter's Prism" }, - { "Glassblower's Bauble", "Glassblower's Bauble" }, - { "Orb of Fusing", "Orb of Fusing" }, - { "Cartographer's Chisel", "Cartographer's Chisel" }, - { "Chromatic Orb", "Chromatic Orb" }, - { "Orb of Augmentation", "Orb of Augmentation" }, - { "Blacksmith's Whetstone", "Blacksmith's Whetstone" }, - { "Orb of Transmutation", "Orb of Transmutation" }, - { "Armourer's Scrap", "Armourer's Scrap" }, - { "Scroll of Wisdom", "Scroll of Wisdom" }, - { "Regal Orb", "Regal Orb" }, - { "Chaos", "Chaos Orb" }, - { "Alch", "Orb of Alchemy" }, - { "Alchs", "Orb of Alchemy" }, - { "Jews", "Jeweller's Orb" }, - { "Jeweller", "Jeweller's Orb" }, - { "Jewellers", "Jeweller's Orb" }, - { "Jeweller's", "Jeweller's Orb" }, - { "X", "Exalted Orb" }, - { "Ex", "Exalted Orb" }, - { "Exalt", "Exalted Orb" }, - { "Exalts", "Exalted Orb" }, - { "Mirror", "Mirror of Kalandra" }, - { "Mirrors", "Mirror of Kalandra" }, - { "Vaal", "Vaal Orb" }, - { "Alt", "Orb of Alteration" }, - { "Alts", "Orb of Alteration" }, - { "Scour", "Orb of Scouring" }, - { "Scours", "Orb of Scouring" }, - { "Divine", "Divine Orb" }, - { "Annul", "Orb of Annulment" }, - { "Annulment", "Orb of Annulment" }, - { "Master Sextant", "Master Cartographer's Sextant" }, - { "Journeyman Sextant", "Journeyman Cartographer's Sextant" }, - { "Apprentice Sextant", "Apprentice Cartographer's Sextant" }, - { "Blessed", "Blessed Orb" }, - { "Regret", "Orb of Regret" }, - { "Regrets", "Orb of Regret" }, - { "Gcp", "Gemcutter's Prism" }, - { "Glassblowers", "Glassblower's Bauble" }, - { "Glassblower's", "Glassblower's Bauble" }, - { "Fusing", "Orb of Fusing" }, - { "Fuses", "Orb of Fusing" }, - { "Fuse", "Orb of Fusing" }, - { "Chisel", "Cartographer's Chisel" }, - { "Chisels", "Cartographer's Chisel" }, - { "Chance", "Orb of Chance" }, - { "Chances", "Orb of Chance" }, - { "Chrome", "Chromatic Orb" }, - { "Chromes", "Chromatic Orb" }, - { "Aug", "Orb of Augmentation" }, - { "Augmentation", "Orb of Augmentation" }, - { "Augment", "Orb of Augmentation" }, - { "Augments", "Orb of Augmentation" }, - { "Whetstone", "Blacksmith's Whetstone" }, - { "Whetstones", "Blacksmith's Whetstone" }, - { "Transmute", "Orb of Transmutation" }, - { "Transmutes", "Orb of Transmutation" }, - { "Armourers", "Armourer's Scrap" }, - { "Armourer's", "Armourer's Scrap" }, - { "Wisdom Scroll", "Scroll of Wisdom" }, - { "Wisdom Scrolls", "Scroll of Wisdom" }, - { "Regal", "Regal Orb" }, - { "Regals", "Regal Orb" } - }; - - public PathOfExileCommands(IHttpClientFactory httpFactory) - => _httpFactory = httpFactory; - - [Cmd] - public async Task PathOfExile(string usr, string league = "", int page = 1) - { - if (--page < 0) - return; - - if (string.IsNullOrWhiteSpace(usr)) - { - await Response().Error("Please provide an account name.").SendAsync(); - return; - } - - var characters = new List(); - - try - { - using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync($"{POE_URL}{usr}"); - characters = JsonConvert.DeserializeObject>(res); - } - catch - { - var embed = _sender.CreateEmbed().WithDescription(GetText(strs.account_not_found)).WithErrorColor(); - - await Response().Embed(embed).SendAsync(); - return; - } - - if (!string.IsNullOrWhiteSpace(league)) - characters.RemoveAll(c => c.League != league); - - await Response() - .Paginated() - .Items(characters) - .PageSize(9) - .CurrentPage(page) - .Page((items, curPage) => - { - var embed = _sender.CreateEmbed() - .WithAuthor($"Characters on {usr}'s account", - "https://web.poecdn.com/image/favicon/ogimage.png", - $"{PROFILE_URL}{usr}") - .WithOkColor(); - - if (characters.Count == 0) - return embed.WithDescription("This account has no characters."); - - var sb = new StringBuilder(); - sb.AppendLine($"```{"#",-5}{"Character Name",-23}{"League",-10}{"Class",-13}{"Level",-3}"); - for (var i = 0; i < items.Count; i++) - { - var character = items[i]; - - sb.AppendLine( - $"#{i + 1 + (curPage * 9),-4}{character.Name,-23}{ShortLeagueName(character.League),-10}{character.Class,-13}{character.Level,-3}"); - } - - sb.AppendLine("```"); - embed.WithDescription(sb.ToString()); - - return embed; - }) - .SendAsync(); - } - - [Cmd] - public async Task PathOfExileLeagues() - { - var leagues = new List(); - - try - { - using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync("http://api.pathofexile.com/leagues?type=main&compact=1"); - leagues = JsonConvert.DeserializeObject>(res); - } - catch - { - var eembed = _sender.CreateEmbed().WithDescription(GetText(strs.leagues_not_found)).WithErrorColor(); - - await Response().Embed(eembed).SendAsync(); - return; - } - - var embed = _sender.CreateEmbed() - .WithAuthor("Path of Exile Leagues", - "https://web.poecdn.com/image/favicon/ogimage.png", - "https://www.pathofexile.com") - .WithOkColor(); - - var sb = new StringBuilder(); - sb.AppendLine($"```{"#",-5}{"League Name",-23}"); - for (var i = 0; i < leagues.Count; i++) - { - var league = leagues[i]; - - sb.AppendLine($"#{i + 1,-4}{league.Id,-23}"); - } - - sb.AppendLine("```"); - - embed.WithDescription(sb.ToString()); - - await Response().Embed(embed).SendAsync(); - } - - [Cmd] - public async Task PathOfExileCurrency( - string leagueName, - string currencyName, - string convertName = "Chaos Orb") - { - if (string.IsNullOrWhiteSpace(leagueName)) - { - await Response().Error("Please provide league name.").SendAsync(); - return; - } - - if (string.IsNullOrWhiteSpace(currencyName)) - { - await Response().Error("Please provide currency name.").SendAsync(); - return; - } - - var cleanCurrency = ShortCurrencyName(currencyName); - var cleanConvert = ShortCurrencyName(convertName); - - try - { - var res = $"{PON_URL}{leagueName}"; - using var http = _httpFactory.CreateClient(); - var obj = JObject.Parse(await http.GetStringAsync(res)); - - var chaosEquivalent = 0.0F; - var conversionEquivalent = 0.0F; - - // poe.ninja API does not include a "chaosEquivalent" property for Chaos Orbs. - if (cleanCurrency == "Chaos Orb") - chaosEquivalent = 1.0F; - else - { - var currencyInput = obj["lines"] - .Values() - .Where(i => i["currencyTypeName"].Value() == cleanCurrency) - .FirstOrDefault(); - chaosEquivalent = float.Parse(currencyInput["chaosEquivalent"].ToString(), - CultureInfo.InvariantCulture); - } - - if (cleanConvert == "Chaos Orb") - conversionEquivalent = 1.0F; - else - { - var currencyOutput = obj["lines"] - .Values() - .Where(i => i["currencyTypeName"].Value() == cleanConvert) - .FirstOrDefault(); - conversionEquivalent = float.Parse(currencyOutput["chaosEquivalent"].ToString(), - CultureInfo.InvariantCulture); - } - - var embed = _sender.CreateEmbed() - .WithAuthor($"{leagueName} Currency Exchange", - "https://web.poecdn.com/image/favicon/ogimage.png", - "http://poe.ninja") - .AddField("Currency Type", cleanCurrency, true) - .AddField($"{cleanConvert} Equivalent", chaosEquivalent / conversionEquivalent, true) - .WithOkColor(); - - await Response().Embed(embed).SendAsync(); - } - catch - { - var embed = _sender.CreateEmbed().WithDescription(GetText(strs.ninja_not_found)).WithErrorColor(); - - await Response().Embed(embed).SendAsync(); - } - } - - private string ShortCurrencyName(string str) - { - if (currencyDictionary.ContainsValue(str)) - return str; - - var currency = currencyDictionary[str]; - - return currency; - } - - private static string ShortLeagueName(string str) - { - var league = str.Replace("Hardcore", "HC", StringComparison.InvariantCultureIgnoreCase); - - return league; - } - } -} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Searches.cs b/src/EllieBot/Modules/Searches/Searches.cs index 04050e6..7fa6067 100644 --- a/src/EllieBot/Modules/Searches/Searches.cs +++ b/src/EllieBot/Modules/Searches/Searches.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Caching.Memory; using EllieBot.Modules.Searches.Common; using EllieBot.Modules.Searches.Services; +using EllieBot.Modules.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SixLabors.ImageSharp; @@ -168,7 +169,7 @@ public partial class Searches : EllieModule .AddField("Rating", movie.ImdbRating, true) .AddField("Genre", movie.Genre, true) .AddField("Year", movie.Year, true) - .WithImageUrl(movie.Poster)) + .WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) ? movie.Poster : null)) .SendAsync(); } diff --git a/src/EllieBot/Modules/Searches/SearchesService.cs b/src/EllieBot/Modules/Searches/SearchesService.cs index f5e3be4..0a9431e 100644 --- a/src/EllieBot/Modules/Searches/SearchesService.cs +++ b/src/EllieBot/Modules/Searches/SearchesService.cs @@ -126,7 +126,7 @@ public class SearchesService : IEService { query = query.Trim().ToLowerInvariant(); - return await _c.GetOrAddAsync(new($"ellie_weather_{query}"), + return await _c.GetOrAddAsync(new($"nadeko_weather_{query}"), async () => await GetWeatherDataFactory(query), TimeSpan.FromHours(3)); } @@ -156,7 +156,7 @@ public class SearchesService : IEService public Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataAsync(string arg) => GetTimeDataFactory(arg); - //return _cache.GetOrAddCachedDataAsync($"ellie_time_{arg}", + //return _cache.GetOrAddCachedDataAsync($"nadeko_time_{arg}", // GetTimeDataFactory, // arg, // TimeSpan.FromMinutes(1)); diff --git a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs index fb64849..c1c73aa 100644 --- a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs +++ b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs @@ -40,7 +40,7 @@ public partial class SearchesConfig : ICloneable [Comment(""" Set the searx instance urls in case you want to use 'searx' for either img or web search. - Ellie will use a random one for each request. + Nadeko will use a random one for each request. Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com` Instances specified must support 'format=json' query parameter. - In case you're running your own searx instance, set @@ -57,7 +57,7 @@ public partial class SearchesConfig : ICloneable [Comment(""" Set the invidious instance urls in case you want to use 'invidious' for `.youtube` search - Ellie will use a random one for each request. + Nadeko will use a random one for each request. These instances may be used for music queue functionality in the future. Use a fully qualified url. Example: https://my-invidious-instance.mydomain.com