From 1ea0e63379c927f29092c1769a1bae4521405c7b Mon Sep 17 00:00:00 2001 From: Toastie Date: Fri, 20 Sep 2024 00:38:05 +1200 Subject: [PATCH] Added .q support for invidious. If you have ytProvider set to invidious in data/searches.yml, invidious will be used to queue up songs and play them work. --- src/EllieBot/EllieBot.csproj | 1 + .../Modules/Gambling/GamblingConfig.cs | 5 +- .../Modules/Music/PlaylistCommands.cs | 3 +- .../Modules/Music/Services/MusicService.cs | 8 +- .../Modules/Music/_common/ITrackInfo.cs | 1 - .../Modules/Music/_common/IYoutubeResolver.cs | 2 +- .../Modules/Music/_common/Impl/MusicPlayer.cs | 40 ++++--- .../Modules/Music/_common/Impl/MusicQueue.cs | 3 - .../Music/_common/Impl/RemoteTrackInfo.cs | 8 +- .../Music/_common/Impl/SimpleTrackInfo.cs | 3 - .../Music/_common/Resolvers/InvTrackInfo.cs | 12 ++ .../Resolvers/InvidiousYoutubeResolver.cs | 108 ++++++++++++++++++ .../_common/Resolvers/TrackResolveProvider.cs | 25 ++-- .../Music/_common/Resolvers/YoutubeHelpers.cs | 10 ++ .../Resolvers/YoutubeResolverFactory.cs | 33 ++++++ .../_common/Resolvers/YtdlYoutubeResolver.cs | 21 ++-- .../Modules/Searches/Search/SearchCommands.cs | 59 +++++----- .../Search/Youtube/InvidiousSearchResponse.cs | 56 ++++++++- .../Youtube/InvidiousYtSearchService.cs | 9 +- .../Searches/_common/Config/SearchesConfig.cs | 5 +- .../_common/Config/SearchesConfigService.cs | 8 ++ src/EllieBot/Modules/Xp/Xp.cs | 3 +- .../Replacements/Impl/ReplacementContext.cs | 1 - .../_common/ServiceCollectionExtensions.cs | 4 +- src/EllieBot/data/searches.yml | 21 ++-- 25 files changed, 345 insertions(+), 104 deletions(-) create mode 100644 src/EllieBot/Modules/Music/_common/Resolvers/InvTrackInfo.cs create mode 100644 src/EllieBot/Modules/Music/_common/Resolvers/InvidiousYoutubeResolver.cs create mode 100644 src/EllieBot/Modules/Music/_common/Resolvers/YoutubeHelpers.cs create mode 100644 src/EllieBot/Modules/Music/_common/Resolvers/YoutubeResolverFactory.cs diff --git a/src/EllieBot/EllieBot.csproj b/src/EllieBot/EllieBot.csproj index ebcd4b2..9bfce54 100644 --- a/src/EllieBot/EllieBot.csproj +++ b/src/EllieBot/EllieBot.csproj @@ -37,6 +37,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/EllieBot/Modules/Gambling/GamblingConfig.cs b/src/EllieBot/Modules/Gambling/GamblingConfig.cs index 2571890..7b6582c 100644 --- a/src/EllieBot/Modules/Gambling/GamblingConfig.cs +++ b/src/EllieBot/Modules/Gambling/GamblingConfig.cs @@ -274,8 +274,8 @@ public sealed partial class WaifuConfig public class WaifuDecayConfig { [Comment(""" - Percentage (0 - 100) of the waifu value to reduce. - Set 0 to disable + Unclaimed waifus will decay by this percentage (0 - 100). + Default is 0 (disabled) For example if a waifu has a price of 500$, setting this value to 10 would reduce the waifu value by 10% (50$) """)] public int UnclaimedDecayPercent { get; set; } = 0; @@ -283,6 +283,7 @@ public sealed partial class WaifuConfig [Comment(""" Claimed waifus will decay by this percentage (0 - 100). Default is 0 (disabled) + For example if a waifu has a price of $500, setting this value to 10 would reduce the waifu by 10% ($50) """)] public int ClaimedDecayPercent { get; set; } = 0; diff --git a/src/EllieBot/Modules/Music/PlaylistCommands.cs b/src/EllieBot/Modules/Music/PlaylistCommands.cs index 0e4137b..d9cc5d4 100644 --- a/src/EllieBot/Modules/Music/PlaylistCommands.cs +++ b/src/EllieBot/Modules/Music/PlaylistCommands.cs @@ -130,13 +130,14 @@ public sealed partial class Music return; } + // todo check locally queued songs var songs = mp.GetQueuedTracks() .Select(s => new PlaylistSong { Provider = s.Platform.ToString(), ProviderType = (MusicType)s.Platform, Title = s.Title, - Query = s.Platform == MusicPlatform.Local ? s.GetStreamUrl().Result!.Trim('"') : s.Url + Query = s.Url }) .ToList(); diff --git a/src/EllieBot/Modules/Music/Services/MusicService.cs b/src/EllieBot/Modules/Music/Services/MusicService.cs index 8495211..89c848d 100644 --- a/src/EllieBot/Modules/Music/Services/MusicService.cs +++ b/src/EllieBot/Modules/Music/Services/MusicService.cs @@ -1,4 +1,5 @@ using EllieBot.Db.Models; +using EllieBot.Modules.Music.Resolvers; using System.Diagnostics.CodeAnalysis; namespace EllieBot.Modules.Music.Services; @@ -8,7 +9,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider private readonly AyuVoiceStateService _voiceStateService; private readonly ITrackResolveProvider _trackResolveProvider; private readonly DbService _db; - private readonly IYoutubeResolver _ytResolver; + private readonly IYoutubeResolverFactory _ytResolver; private readonly ILocalTrackResolver _localResolver; private readonly DiscordSocketClient _client; private readonly IBotStrings _strings; @@ -24,7 +25,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider AyuVoiceStateService voiceStateService, ITrackResolveProvider trackResolveProvider, DbService db, - IYoutubeResolver ytResolver, + IYoutubeResolverFactory ytResolver, ILocalTrackResolver localResolver, DiscordSocketClient client, IBotStrings strings, @@ -93,7 +94,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider public async Task EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer) { var count = 0; - await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query)) + await foreach (var track in _ytResolver.GetYoutubeResolver().ResolveTracksFromPlaylistAsync(query)) { if (mp.IsKilled) break; @@ -139,6 +140,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider var mp = new MusicPlayer(queue, resolver, + _ytResolver, proxy, _googleApiService, settings.QualityPreset, diff --git a/src/EllieBot/Modules/Music/_common/ITrackInfo.cs b/src/EllieBot/Modules/Music/_common/ITrackInfo.cs index 347e8fa..3525b59 100644 --- a/src/EllieBot/Modules/Music/_common/ITrackInfo.cs +++ b/src/EllieBot/Modules/Music/_common/ITrackInfo.cs @@ -8,5 +8,4 @@ public interface ITrackInfo public string Thumbnail { get; } public TimeSpan Duration { get; } public MusicPlatform Platform { get; } - public ValueTask GetStreamUrl(); } \ No newline at end of file diff --git a/src/EllieBot/Modules/Music/_common/IYoutubeResolver.cs b/src/EllieBot/Modules/Music/_common/IYoutubeResolver.cs index 433012d..e4c2f53 100644 --- a/src/EllieBot/Modules/Music/_common/IYoutubeResolver.cs +++ b/src/EllieBot/Modules/Music/_common/IYoutubeResolver.cs @@ -4,8 +4,8 @@ namespace EllieBot.Modules.Music; public interface IYoutubeResolver : IPlatformQueryResolver { - public Regex YtVideoIdRegex { get; } public Task ResolveByIdAsync(string id); IAsyncEnumerable ResolveTracksFromPlaylistAsync(string query); Task ResolveByQueryAsync(string query, bool tryExtractingId); + Task GetStreamUrl(string query); } \ No newline at end of file diff --git a/src/EllieBot/Modules/Music/_common/Impl/MusicPlayer.cs b/src/EllieBot/Modules/Music/_common/Impl/MusicPlayer.cs index 6819d4e..0edd3cf 100644 --- a/src/EllieBot/Modules/Music/_common/Impl/MusicPlayer.cs +++ b/src/EllieBot/Modules/Music/_common/Impl/MusicPlayer.cs @@ -1,5 +1,6 @@ using EllieBot.Voice; using EllieBot.Db.Models; +using EllieBot.Modules.Music.Resolvers; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -27,6 +28,7 @@ public sealed class MusicPlayer : IMusicPlayer private readonly IMusicQueue _queue; private readonly ITrackResolveProvider _trackResolveProvider; + private readonly IYoutubeResolverFactory _ytResolverFactory; private readonly IVoiceProxy _proxy; private readonly IGoogleApiService _googleApiService; private readonly ISongBuffer _songBuffer; @@ -41,6 +43,7 @@ public sealed class MusicPlayer : IMusicPlayer public MusicPlayer( IMusicQueue queue, ITrackResolveProvider trackResolveProvider, + IYoutubeResolverFactory ytResolverFactory, IVoiceProxy proxy, IGoogleApiService googleApiService, QualityPreset qualityPreset, @@ -48,6 +51,7 @@ public sealed class MusicPlayer : IMusicPlayer { _queue = queue; _trackResolveProvider = trackResolveProvider; + _ytResolverFactory = ytResolverFactory; _proxy = proxy; _googleApiService = googleApiService; AutoPlay = autoPlay; @@ -118,7 +122,7 @@ public sealed class MusicPlayer : IMusicPlayer // make sure song buffer is ready to be (re)used _songBuffer.Reset(); - var streamUrl = await track.GetStreamUrl(); + var streamUrl = await GetStreamUrl(track); // start up the data source using var source = FfmpegTrackDataSource.CreateAsync( _vc.BitDepth, @@ -256,6 +260,7 @@ public sealed class MusicPlayer : IMusicPlayer IsStopped = true; Log.Error("Please install ffmpeg and make sure it's added to your " + "PATH environment variable before trying again"); + } catch (OperationCanceledException) { @@ -264,6 +269,7 @@ public sealed class MusicPlayer : IMusicPlayer catch (Exception ex) { Log.Error(ex, "Unknown error in music loop: {ErrorMessage}", ex.Message); + await Task.Delay(3_000); } finally { @@ -303,6 +309,14 @@ public sealed class MusicPlayer : IMusicPlayer } } + private async Task GetStreamUrl(IQueuedTrackInfo track) + { + if (track.TrackInfo is SimpleTrackInfo sti) + return sti.StreamUrl; + + return await _ytResolverFactory.GetYoutubeResolver().GetStreamUrl(track.TrackInfo.Id); + } + private bool? CopyChunkToOutput(ISongBuffer sb, VoiceClient vc) { var data = sb.Read(vc.InputLength, out var length); @@ -406,19 +420,19 @@ public sealed class MusicPlayer : IMusicPlayer break; await chunk.Select(async data => + { + var (query, platform) = data; + try { - var (query, platform) = data; - try - { - await TryEnqueueTrackAsync(query, queuer, false, platform); - errorCount = 0; - } - catch (Exception ex) - { - Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query); - ++errorCount; - } - }) + await TryEnqueueTrackAsync(query, queuer, false, platform); + errorCount = 0; + } + catch (Exception ex) + { + Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query); + ++errorCount; + } + }) .WhenAll(); await Task.Delay(1000); diff --git a/src/EllieBot/Modules/Music/_common/Impl/MusicQueue.cs b/src/EllieBot/Modules/Music/_common/Impl/MusicQueue.cs index 1b1ce9c..9b1c7aa 100644 --- a/src/EllieBot/Modules/Music/_common/Impl/MusicQueue.cs +++ b/src/EllieBot/Modules/Music/_common/Impl/MusicQueue.cs @@ -28,9 +28,6 @@ public sealed partial class MusicQueue TrackInfo = trackInfo; Queuer = queuer; } - - public ValueTask GetStreamUrl() - => TrackInfo.GetStreamUrl(); } } diff --git a/src/EllieBot/Modules/Music/_common/Impl/RemoteTrackInfo.cs b/src/EllieBot/Modules/Music/_common/Impl/RemoteTrackInfo.cs index b002779..c6fbf10 100644 --- a/src/EllieBot/Modules/Music/_common/Impl/RemoteTrackInfo.cs +++ b/src/EllieBot/Modules/Music/_common/Impl/RemoteTrackInfo.cs @@ -6,11 +6,5 @@ public sealed record RemoteTrackInfo( string Url, string Thumbnail, TimeSpan Duration, - MusicPlatform Platform, - Func> _streamFactory) : ITrackInfo -{ - private readonly Func> _streamFactory = _streamFactory; - public async ValueTask GetStreamUrl() - => await _streamFactory(); -} \ No newline at end of file + MusicPlatform Platform) : ITrackInfo; \ No newline at end of file diff --git a/src/EllieBot/Modules/Music/_common/Impl/SimpleTrackInfo.cs b/src/EllieBot/Modules/Music/_common/Impl/SimpleTrackInfo.cs index 9ae1c30..c7828cc 100644 --- a/src/EllieBot/Modules/Music/_common/Impl/SimpleTrackInfo.cs +++ b/src/EllieBot/Modules/Music/_common/Impl/SimpleTrackInfo.cs @@ -24,7 +24,4 @@ public sealed class SimpleTrackInfo : ITrackInfo Platform = platform; StreamUrl = streamUrl; } - - public ValueTask GetStreamUrl() - => new(StreamUrl); } \ No newline at end of file diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/InvTrackInfo.cs b/src/EllieBot/Modules/Music/_common/Resolvers/InvTrackInfo.cs new file mode 100644 index 0000000..cca2727 --- /dev/null +++ b/src/EllieBot/Modules/Music/_common/Resolvers/InvTrackInfo.cs @@ -0,0 +1,12 @@ +namespace EllieBot.Modules.Music; + +public sealed class InvTrackInfo : ITrackInfo +{ + public required string Id { get; init; } + public required string Title { get; init; } + public required string Url { get; init; } + public required string Thumbnail { get; init; } + public required TimeSpan Duration { get; init; } + public required MusicPlatform Platform { get; init; } + public required string? StreamUrl { get; init; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/InvidiousYoutubeResolver.cs b/src/EllieBot/Modules/Music/_common/Resolvers/InvidiousYoutubeResolver.cs new file mode 100644 index 0000000..243fd13 --- /dev/null +++ b/src/EllieBot/Modules/Music/_common/Resolvers/InvidiousYoutubeResolver.cs @@ -0,0 +1,108 @@ +using EllieBot.Modules.Searches; +using System.Net.Http.Json; + +namespace EllieBot.Modules.Music; + +public sealed class InvidiousYoutubeResolver : IYoutubeResolver +{ + private readonly IHttpClientFactory _httpFactory; + private readonly SearchesConfigService _sc; + private readonly EllieRandom _rng; + + private string InvidiousApiUrl + => _sc.Data.InvidiousInstances[_rng.Next(0, _sc.Data.InvidiousInstances.Count)]; + + public InvidiousYoutubeResolver(IHttpClientFactory httpFactory, SearchesConfigService sc) + { + _rng = new EllieRandom(); + _httpFactory = httpFactory; + _sc = sc; + } + + public async Task ResolveByQueryAsync(string query) + { + using var http = _httpFactory.CreateClient(); + + var items = await http.GetFromJsonAsync>( + $"{InvidiousApiUrl}/api/v1/search" + + $"?q={query}" + + $"&type=video"); + + if (items is null || items.Count == 0) + return null; + + + var res = items.First(); + + return new InvTrackInfo() + { + Id = res.VideoId, + Title = res.Title, + Url = $"https://youtube.com/watch?v={res.VideoId}", + Thumbnail = res.Thumbnails?.Select(x => x.Url).FirstOrDefault() ?? string.Empty, + Duration = TimeSpan.FromSeconds(res.LengthSeconds), + Platform = MusicPlatform.Youtube, + StreamUrl = null, + }; + } + + public async Task ResolveByIdAsync(string id) + => await InternalResolveByIdAsync(id); + + private async Task InternalResolveByIdAsync(string id) + { + using var http = _httpFactory.CreateClient(); + + var res = await http.GetFromJsonAsync( + $"{InvidiousApiUrl}/api/v1/videos/{id}"); + + if (res is null) + return null; + + return new InvTrackInfo() + { + Id = res.VideoId, + Title = res.Title, + Url = $"https://youtube.com/watch?v={res.VideoId}", + Thumbnail = res.Thumbnails?.Select(x => x.Url).FirstOrDefault() ?? string.Empty, + Duration = TimeSpan.FromSeconds(res.LengthSeconds), + Platform = MusicPlatform.Youtube, + StreamUrl = res.AdaptiveFormats.FirstOrDefault(x => x.AudioQuality == "AUDIO_QUALITY_HIGH")?.Url + ?? res.AdaptiveFormats.FirstOrDefault(x => x.AudioQuality == "AUDIO_QUALITY_MEDIUM")?.Url + ?? res.AdaptiveFormats.FirstOrDefault(x => x.AudioQuality == "AUDIO_QUALITY_LOW")?.Url + }; + } + + public async IAsyncEnumerable ResolveTracksFromPlaylistAsync(string query) + { + using var http = _httpFactory.CreateClient(); + var res = await http.GetFromJsonAsync( + $"{InvidiousApiUrl}/api/v1/search?type=video&q={query}"); + + if (res is null) + yield break; + + foreach (var video in res.Videos) + { + yield return new InvTrackInfo() + { + Id = video.VideoId, + Title = video.Title, + Url = $"https://youtube.com/watch?v={video.VideoId}", + Thumbnail = video.Thumbnails?.Select(x => x.Url).FirstOrDefault() ?? string.Empty, + Duration = TimeSpan.FromSeconds(video.LengthSeconds), + Platform = MusicPlatform.Youtube, + StreamUrl = null + }; + } + } + + public Task ResolveByQueryAsync(string query, bool tryExtractingId) + => ResolveByQueryAsync(query); + + public async Task GetStreamUrl(string videoId) + { + var video = await InternalResolveByIdAsync(videoId); + return video?.StreamUrl; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/TrackResolveProvider.cs b/src/EllieBot/Modules/Music/_common/Resolvers/TrackResolveProvider.cs index 642edf1..21a6adf 100644 --- a/src/EllieBot/Modules/Music/_common/Resolvers/TrackResolveProvider.cs +++ b/src/EllieBot/Modules/Music/_common/Resolvers/TrackResolveProvider.cs @@ -1,13 +1,15 @@ -namespace EllieBot.Modules.Music; +using EllieBot.Modules.Music.Resolvers; + +namespace EllieBot.Modules.Music; public sealed class TrackResolveProvider : ITrackResolveProvider { - private readonly IYoutubeResolver _ytResolver; + private readonly IYoutubeResolverFactory _ytResolver; private readonly ILocalTrackResolver _localResolver; private readonly IRadioResolver _radioResolver; public TrackResolveProvider( - IYoutubeResolver ytResolver, + IYoutubeResolverFactory ytResolver, ILocalTrackResolver localResolver, IRadioResolver radioResolver) { @@ -23,19 +25,22 @@ public sealed class TrackResolveProvider : ITrackResolveProvider case MusicPlatform.Radio: return _radioResolver.ResolveByQueryAsync(query); case MusicPlatform.Youtube: - return _ytResolver.ResolveByQueryAsync(query); + return _ytResolver.GetYoutubeResolver().ResolveByQueryAsync(query); case MusicPlatform.Local: return _localResolver.ResolveByQueryAsync(query); case null: - var match = _ytResolver.YtVideoIdRegex.Match(query); + var match = YoutubeHelpers.YtVideoIdRegex.Match(query); + if (match.Success) - return _ytResolver.ResolveByIdAsync(match.Groups["id"].Value); - else if (Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.IsFile) + return _ytResolver.GetYoutubeResolver().ResolveByIdAsync(match.Groups["id"].Value); + + if (Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.IsFile) return _localResolver.ResolveByQueryAsync(uri.AbsolutePath); - else if (IsRadioLink(query)) + + if (IsRadioLink(query)) return _radioResolver.ResolveByQueryAsync(query); - else - return _ytResolver.ResolveByQueryAsync(query, false); + + return _ytResolver.GetYoutubeResolver().ResolveByQueryAsync(query, false); default: Log.Error("Unsupported platform: {MusicPlatform}", forcePlatform); return Task.FromResult(null); diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeHelpers.cs b/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeHelpers.cs new file mode 100644 index 0000000..869102d --- /dev/null +++ b/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeHelpers.cs @@ -0,0 +1,10 @@ +using System.Text.RegularExpressions; + +namespace EllieBot.Modules.Music; + +public sealed class YoutubeHelpers +{ + public static Regex YtVideoIdRegex { get; } = + new(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})", + RegexOptions.Compiled); +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeResolverFactory.cs b/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeResolverFactory.cs new file mode 100644 index 0000000..955faca --- /dev/null +++ b/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeResolverFactory.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection; +using EllieBot.Modules.Searches; +using EllieBot.Modules.Searches.Services; + +namespace EllieBot.Modules.Music.Resolvers; + +public interface IYoutubeResolverFactory +{ + IYoutubeResolver GetYoutubeResolver(); +} + +public sealed class YoutubeResolverFactory : IYoutubeResolverFactory +{ + private readonly SearchesConfigService _ss; + private readonly IServiceProvider _services; + + public YoutubeResolverFactory(SearchesConfigService ss, IServiceProvider services) + { + _ss = ss; + _services = services; + } + + public IYoutubeResolver GetYoutubeResolver() + { + var conf = _ss.Data; + if (conf.YtProvider == YoutubeSearcher.Invidious) + { + return _services.GetRequiredService(); + } + + return _services.GetRequiredService(); + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs b/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs index 70479d0..eeb3a1c 100644 --- a/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs +++ b/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs @@ -16,9 +16,6 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver private static readonly Regex _simplePlaylistRegex = new(@"&list=(?[\w\-]{12,})", RegexOptions.Compiled); - public Regex YtVideoIdRegex { get; } = - new(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})", - RegexOptions.Compiled); private readonly ITrackCacher _trackCacher; @@ -32,7 +29,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver { _trackCacher = trackCacher; _google = google; - + _ytdlPlaylistOperation = new("-4 " + "--geo-bypass " @@ -46,7 +43,8 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver + "--no-check-certificate " + "-i " + "--yes-playlist " - + "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl); + + "-- \"{0}\"", + scs.Data.YtProvider != YoutubeSearcher.Ytdl); _ytdlIdOperation = new("-4 " + "--geo-bypass " @@ -58,7 +56,8 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver + "--get-thumbnail " + "--get-duration " + "--no-check-certificate " - + "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl); + + "-- \"{0}\"", + scs.Data.YtProvider != YoutubeSearcher.Ytdl); _ytdlSearchOperation = new("-4 " + "--geo-bypass " @@ -71,7 +70,8 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver + "--get-duration " + "--no-check-certificate " + "--default-search " - + "\"ytsearch:\" -- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl); + + "\"ytsearch:\" -- \"{0}\"", + scs.Data.YtProvider != YoutubeSearcher.Ytdl); } private YtTrackData ResolveYtdlData(string ytdlOutputString) @@ -102,8 +102,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver $"https://youtube.com/watch?v={trackData.Id}", trackData.Thumbnail, trackData.Duration, - MusicPlatform.Youtube, - CreateCacherFactory(trackData.Id)); + MusicPlatform.Youtube); private Func> CreateCacherFactory(string id) => () => _trackCacher.GetOrCreateStreamLink(id, @@ -268,7 +267,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver { if (tryResolving) { - var match = YtVideoIdRegex.Match(query); + var match = YoutubeHelpers.YtVideoIdRegex.Match(query); if (match.Success) return await ResolveByIdAsync(match.Groups["id"].Value); } @@ -290,6 +289,8 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver return DataToInfo(new(cachedData.Title, cachedData.Id, cachedData.Thumbnail, null, cachedData.Duration)); } + public Task GetStreamUrl(string videoId) + => CreateCacherFactory(videoId)(); private readonly struct YtTrackData { public readonly string Title; diff --git a/src/EllieBot/Modules/Searches/Search/SearchCommands.cs b/src/EllieBot/Modules/Searches/Search/SearchCommands.cs index 411600c..b155c75 100644 --- a/src/EllieBot/Modules/Searches/Search/SearchCommands.cs +++ b/src/EllieBot/Modules/Searches/Search/SearchCommands.cs @@ -60,21 +60,21 @@ public partial class Searches descStr = descStr.TrimTo(4096); var embed = _sender.CreateEmbed() - .WithOkColor() - .WithAuthor(ctx.User) - .WithTitle(query.TrimTo(64)!) - .WithDescription(descStr) - .WithFooter( - GetText(strs.results_in(data.Info.TotalResults, data.Info.SearchTime)), - "https://i.imgur.com/G46fm8J.png"); + .WithOkColor() + .WithAuthor(ctx.User) + .WithTitle(query.TrimTo(64)!) + .WithDescription(descStr) + .WithFooter( + GetText(strs.results_in(data.Info.TotalResults, data.Info.SearchTime)), + "https://i.imgur.com/G46fm8J.png"); await Response().Embed(embed).SendAsync(); } [Cmd] - public async Task Image([Leftover] string? query) + public async Task Image([Leftover] string query) { - query = query?.Trim(); + query = query.Trim(); if (string.IsNullOrWhiteSpace(query)) { @@ -99,29 +99,32 @@ public partial class Searches EmbedBuilder CreateEmbed(IImageSearchResultEntry entry) { return _sender.CreateEmbed() - .WithOkColor() - .WithAuthor(ctx.User) - .WithTitle(query) - .WithUrl("https://google.com") - .WithImageUrl(entry.Link); + .WithOkColor() + .WithAuthor(ctx.User) + .WithTitle(query) + .WithUrl("https://google.com") + .WithImageUrl(entry.Link); } - embeds.Add(CreateEmbed(data.Entries.First()) - .WithFooter( - GetText(strs.results_in(data.Info.TotalResults, data.Info.SearchTime)), - "https://i.imgur.com/G46fm8J.png")); + await Response() + .Paginated() + .Items(data.Entries) + .PageSize(1) + .AddFooter(false) + .Page((items, _) => + { + var item = items.FirstOrDefault(); - var random = data.Entries.Skip(1) - .Shuffle() - .Take(3) - .ToArray(); + if (item is null) + return _sender.CreateEmbed() + .WithDescription(GetText(strs.no_search_results)); - foreach (var entry in random) - { - embeds.Add(CreateEmbed(entry)); - } + var embed = CreateEmbed(item); + embeds.Add(embed); - await Response().Embeds(embeds).SendAsync(); + return embed; + }) + .SendAsync(); } private TypedKey GetYtCacheKey(string query) @@ -158,7 +161,7 @@ public partial class Searches var maybeResult = await GetYoutubeUrlFromCacheAsync(query) ?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query); - if (maybeResult is not {} result || result is {Url: null}) + if (maybeResult is not { } result || result is { Url: null }) { await Response().Error(strs.no_results).SendAsync(); return; diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs b/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs index 9951db8..9b18d0c 100644 --- a/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs +++ b/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs @@ -5,5 +5,59 @@ namespace EllieBot.Modules.Searches; public sealed class InvidiousSearchResponse { [JsonPropertyName("videoId")] - public string VideoId { get; set; } = null!; + public required string VideoId { get; init; } + + [JsonPropertyName("title")] + public required string Title { get; init; } + + [JsonPropertyName("videoThumbnails")] + public required List Thumbnails { get; init; } + + [JsonPropertyName("lengthSeconds")] + public required int LengthSeconds { get; init; } + + [JsonPropertyName("description")] + public required string Description { get; init; } +} + +public sealed class InvidiousVideoResponse +{ + [JsonPropertyName("title")] + public required string Title { get; init; } + + [JsonPropertyName("videoId")] + public required string VideoId { get; init; } + + [JsonPropertyName("lengthSeconds")] + public required int LengthSeconds { get; init; } + + [JsonPropertyName("videoThumbnails")] + public required List Thumbnails { get; init; } + + [JsonPropertyName("adaptiveFormats")] + public required List AdaptiveFormats { get; init; } +} + +public sealed class InvidiousAdaptiveFormat +{ + [JsonPropertyName("url")] + public required string Url { get; init; } + + [JsonPropertyName("audioQuality")] + public string? AudioQuality { get; init; } +} + +public sealed class InvidiousPlaylistResponse +{ + [JsonPropertyName("title")] + public required string Title { get; init; } + + [JsonPropertyName("videos")] + public required List Videos { get; init; } +} + +public sealed class InvidiousThumbnail +{ + [JsonPropertyName("url")] + public required string Url { get; init; } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs b/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs index 6fc8bac..17bd519 100644 --- a/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs +++ b/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs @@ -32,13 +32,14 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, IEService var instance = instances[_rng.Next(0, instances.Count)]; + var url = $"{instance}/api/v1/search" + + $"?q={query}" + + $"&type=video"; using var http = _http.CreateClient(); var res = await http.GetFromJsonAsync>( - $"{instance}/api/v1/search" - + $"?q={query}" - + $"&type=video"); + url); - if (res is null or {Count: 0}) + if (res is null or { Count: 0 }) return null; return new VideoInfo(res[0].VideoId); diff --git a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs index fb64849..8cb0227 100644 --- a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs +++ b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs @@ -7,7 +7,7 @@ namespace EllieBot.Modules.Searches; public partial class SearchesConfig : ICloneable { [Comment("DO NOT CHANGE")] - public int Version { get; set; } = 0; + public int Version { get; set; } = 3; [Comment(""" Which engine should .search command @@ -26,7 +26,7 @@ public partial class SearchesConfig : ICloneable [Comment(""" - Which search provider will be used for the `.youtube` command. + Which search provider will be used for the `.youtube` and `.q` commands. - `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console @@ -58,7 +58,6 @@ 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. - These instances may be used for music queue functionality in the future. Use a fully qualified url. Example: https://my-invidious-instance.mydomain.com Instances specified must have api available. diff --git a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfigService.cs b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfigService.cs index f656a5b..39ae3c6 100644 --- a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfigService.cs +++ b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfigService.cs @@ -54,5 +54,13 @@ public class SearchesConfigService : ConfigServiceBase c.Version = 2; }); } + + if (data.Version < 3) + { + ModifyConfig(c => + { + c.Version = 3; + }); + } } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/Xp.cs b/src/EllieBot/Modules/Xp/Xp.cs index 63e59e3..e961d87 100644 --- a/src/EllieBot/Modules/Xp/Xp.cs +++ b/src/EllieBot/Modules/Xp/Xp.cs @@ -445,9 +445,10 @@ public partial class Xp : EllieModule if (!string.IsNullOrWhiteSpace(item.Desc)) eb.AddField(GetText(strs.desc), item.Desc); +#if GLOBAL_ELLIE if (key == "default") eb.WithDescription(GetText(strs.xpshop_website)); - +#endif var tier = _service.GetXpShopTierRequirement(type); if (tier != PatronTier.None) diff --git a/src/EllieBot/_common/Replacements/Impl/ReplacementContext.cs b/src/EllieBot/_common/Replacements/Impl/ReplacementContext.cs index 184ba4e..4e87ce9 100644 --- a/src/EllieBot/_common/Replacements/Impl/ReplacementContext.cs +++ b/src/EllieBot/_common/Replacements/Impl/ReplacementContext.cs @@ -54,7 +54,6 @@ public sealed class ReplacementContext public ReplacementContext WithOverride(string key, Func repFactory) => WithOverride(key, () => new ValueTask(repFactory())); - public ReplacementContext WithOverride(Regex regex, Func> repFactory) { if (_regexPatterns.Add(regex.ToString())) diff --git a/src/EllieBot/_common/ServiceCollectionExtensions.cs b/src/EllieBot/_common/ServiceCollectionExtensions.cs index 3a59226..c5539af 100644 --- a/src/EllieBot/_common/ServiceCollectionExtensions.cs +++ b/src/EllieBot/_common/ServiceCollectionExtensions.cs @@ -51,7 +51,9 @@ public static class ServiceCollectionExtensions svcs.RegisterMany(Reuse.Singleton); svcs.AddSingleton(); - svcs.AddSingleton(); + svcs.AddSingleton(); + svcs.AddSingleton(); + svcs.AddSingleton(); svcs.AddSingleton(); svcs.AddSingleton(); svcs.AddSingleton(); diff --git a/src/EllieBot/data/searches.yml b/src/EllieBot/data/searches.yml index 9b7ab5c..f551bae 100644 --- a/src/EllieBot/data/searches.yml +++ b/src/EllieBot/data/searches.yml @@ -1,5 +1,5 @@ # DO NOT CHANGE -version: 2 +version: 3 # Which engine should .search command # 'google_scrape' - default. Scrapes the webpage for results. May break. Requires no api keys. # 'google' - official google api. Requires googleApiKey and google.searchId set in creds.yml @@ -9,14 +9,14 @@ webSearchEngine: Google_Scrape # 'google'- official google api. googleApiKey and google.imageSearchId set in creds.yml # 'searx' requires at least one searx instance specified in the 'searxInstances' property below imgSearchEngine: Google -# Which search provider will be used for the `.youtube` command. -# +# Which search provider will be used for the `.youtube` and `.q` commands. +# # - `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console -# +# # - `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow. -# +# # - `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables -# +# # - `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property ytProvider: Ytdlp # Set the searx instance urls in case you want to use 'searx' for either img or web search. @@ -24,20 +24,19 @@ ytProvider: Ytdlp # 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 -# +# # search: # formats: # - json -# +# # in 'searxng/settings.yml' on your server -# +# # - If you're using a public instance, make sure that the instance you're using supports it (they usually don't) searxInstances: [] # Set the invidious instance urls in case you want to use 'invidious' for `.youtube` search # Ellie 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 -# +# # Instances specified must have api available. # You check that by opening an api endpoint in your browser. For example: https://my-invidious-instance.mydomain.com/api/v1/trending invidiousInstances: []